├── .github ├── pull_request_template.md └── workflows │ ├── deploy_docs.yml │ ├── push_gem.yml │ └── test.yml ├── .gitignore ├── .mdlrc ├── .rspec ├── .rubocop.yml ├── .simplecov ├── .yardopts ├── AUTHORS ├── Appraisals ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── UPGRADING.md ├── bin ├── console.rb └── smoke.rb ├── gemfiles ├── openssl.gemfile └── standalone.gemfile ├── lib ├── jwt.rb └── jwt │ ├── base64.rb │ ├── claims.rb │ ├── claims │ ├── audience.rb │ ├── crit.rb │ ├── decode_verifier.rb │ ├── expiration.rb │ ├── issued_at.rb │ ├── issuer.rb │ ├── jwt_id.rb │ ├── not_before.rb │ ├── numeric.rb │ ├── required.rb │ ├── subject.rb │ └── verifier.rb │ ├── configuration.rb │ ├── configuration │ ├── container.rb │ ├── decode_configuration.rb │ └── jwk_configuration.rb │ ├── decode.rb │ ├── encode.rb │ ├── encoded_token.rb │ ├── error.rb │ ├── json.rb │ ├── jwa.rb │ ├── jwa │ ├── ecdsa.rb │ ├── hmac.rb │ ├── none.rb │ ├── ps.rb │ ├── rsa.rb │ ├── signing_algorithm.rb │ └── unsupported.rb │ ├── jwk.rb │ ├── jwk │ ├── ec.rb │ ├── hmac.rb │ ├── key_base.rb │ ├── key_finder.rb │ ├── kid_as_key_digest.rb │ ├── rsa.rb │ ├── set.rb │ └── thumbprint.rb │ ├── token.rb │ ├── version.rb │ └── x5c_key_finder.rb ├── ruby-jwt.gemspec └── spec ├── fixtures └── keys │ ├── ec256-private-v2.pem │ ├── ec256-private.pem │ ├── ec256-public-v2.pem │ ├── ec256-public.pem │ ├── ec256-wrong-public.pem │ ├── ec256k-private.pem │ ├── ec256k-public.pem │ ├── ec384-private.pem │ ├── ec384-public.pem │ ├── ec512-private.pem │ ├── ec512-public.pem │ ├── rsa-2048-private.pem │ ├── rsa-2048-public.pem │ └── rsa-2048-wrong-public.pem ├── integration └── readme_examples_spec.rb ├── jwt ├── claims │ ├── audience_spec.rb │ ├── crit_spec.rb │ ├── expiration_spec.rb │ ├── issued_at_spec.rb │ ├── issuer_spec.rb │ ├── jwt_id_spec.rb │ ├── not_before_spec.rb │ ├── numeric_spec.rb │ ├── required_spec.rb │ └── verifier_spec.rb ├── claims_spec.rb ├── configuration │ └── jwk_configuration_spec.rb ├── configuration_spec.rb ├── encoded_token_spec.rb ├── jwa │ ├── ecdsa_spec.rb │ ├── hmac_spec.rb │ ├── none_spec.rb │ ├── ps_spec.rb │ ├── rsa_spec.rb │ └── unsupported_spec.rb ├── jwa_spec.rb ├── jwk │ ├── decode_with_jwk_spec.rb │ ├── ec_spec.rb │ ├── hmac_spec.rb │ ├── rsa_spec.rb │ ├── set_spec.rb │ └── thumbprint_spec.rb ├── jwk_spec.rb ├── jwt_spec.rb ├── token_spec.rb ├── version_spec.rb └── x5c_key_finder_spec.rb ├── spec_helper.rb └── spec_support ├── test_keys.rb └── token.rb /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | ### Description 3 | 4 | This Pull Request changes/fixes this thing 5 | 6 | ### Checklist 7 | 8 | Before the PR can be merged be sure the following are checked: 9 | * [ ] There are tests for the fix or feature added/changed 10 | * [ ] 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) 11 | -------------------------------------------------------------------------------- /.github/workflows/deploy_docs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: GitHub Pages 3 | 4 | on: 5 | push: 6 | branches: 7 | - "main" 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Ruby 17 | uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: ruby 20 | bundler-cache: true 21 | - name: Install yard 22 | run: gem install yard 23 | - name: Install redcarpet 24 | run: gem install redcarpet 25 | - name: Build docs 26 | run: yard 27 | - name: Configure CNAME 28 | run: echo "ruby-jwt.org" > ./doc/CNAME 29 | - name: Deploy 30 | uses: peaceiris/actions-gh-pages@v4 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | publish_dir: ./doc 34 | -------------------------------------------------------------------------------- /.github/workflows/push_gem.yml: -------------------------------------------------------------------------------- 1 | --- 2 | "on": 3 | push: 4 | tags: 5 | - v* 6 | name: Push Gem 7 | jobs: 8 | push: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | id-token: write 13 | steps: 14 | - uses: rubygems/configure-rubygems-credentials@main 15 | with: 16 | role-to-assume: ${{ secrets.RUBYGEMS_PUSH_ROLE }} 17 | - uses: actions/checkout@v4 18 | - name: Set remote URL 19 | run: | 20 | # Attribute commits to the last committer on HEAD 21 | git config --global user.email "$(git log -1 --pretty=format:'%ae')" 22 | git config --global user.name "$(git log -1 --pretty=format:'%an')" 23 | git remote set-url origin "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY" 24 | - name: Set up Ruby 25 | uses: ruby/setup-ruby@v1 26 | with: 27 | bundler-cache: true 28 | ruby-version: ruby 29 | - name: Release 30 | run: bundle exec rake release 31 | - name: Wait for release to propagate 32 | run: | 33 | gem install rubygems-await 34 | gem_tuple="$(ruby -rbundler/setup -rbundler -e ' 35 | spec = Bundler.definition.specs.find {|s| s.name == ARGV[0] } 36 | raise "No spec for #{ARGV[0]}" unless spec 37 | print [spec.name, spec.version, spec.platform].join(":") 38 | ' "jwt")" 39 | gem await "${gem_tuple}" 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | branches: 9 | - "*" 10 | jobs: 11 | rubocop: 12 | name: RuboCop 13 | timeout-minutes: 30 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Ruby 18 | uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: ruby 21 | bundler-cache: true 22 | - name: Run RuboCop 23 | run: bundle exec rubocop 24 | markdown: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | - name: Lint Markdown 30 | uses: actionshub/markdownlint@v3.1.4 31 | test: 32 | name: ${{ matrix.os }} - Ruby ${{ matrix.ruby }} - ${{ matrix.gemfile }} 33 | runs-on: ${{ matrix.os }} 34 | strategy: 35 | fail-fast: false 36 | matrix: 37 | os: 38 | - ubuntu-latest 39 | ruby: 40 | - "2.5" 41 | - "2.6" 42 | - "2.7" 43 | - "3.0" 44 | - "3.1" 45 | - "3.2" 46 | - "3.3" 47 | - "3.4" 48 | gemfile: 49 | - gemfiles/standalone.gemfile 50 | experimental: [false] 51 | include: 52 | - os: ubuntu-latest 53 | ruby: "3.0" 54 | gemfile: "gemfiles/openssl.gemfile" 55 | experimental: false 56 | - os: ubuntu-latest 57 | ruby: "truffleruby-head" 58 | gemfile: "gemfiles/standalone.gemfile" 59 | experimental: true 60 | - os: ubuntu-latest 61 | ruby: "head" 62 | gemfile: "gemfiles/standalone.gemfile" 63 | experimental: true 64 | continue-on-error: ${{ matrix.experimental }} 65 | env: 66 | BUNDLE_GEMFILE: ${{ matrix.gemfile }} 67 | 68 | steps: 69 | - uses: actions/checkout@v4 70 | 71 | - name: Set up Ruby 72 | uses: ruby/setup-ruby@v1 73 | with: 74 | ruby-version: ${{ matrix.ruby }} 75 | bundler-cache: true 76 | 77 | - name: Run tests 78 | run: bundle exec rspec 79 | 80 | - name: Sanitize gemfile path 81 | run: echo "SANITIZED_GEMFILE=${{ matrix.gemfile }}" | tr '/' '-' >> $GITHUB_ENV 82 | 83 | - name: Upload test coverage folder for later reporting 84 | uses: actions/upload-artifact@v4 85 | with: 86 | name: coverage-${{ matrix.os }}-${{ matrix.ruby }}-${{ env.SANITIZED_GEMFILE }} 87 | path: coverage/*.json 88 | retention-days: 1 89 | 90 | coverage: 91 | name: Report coverage to Qlty 92 | runs-on: ubuntu-latest 93 | needs: test 94 | if: success() 95 | steps: 96 | - uses: actions/checkout@v4 97 | 98 | - name: Download coverage reports from the test job 99 | uses: actions/download-artifact@v4 100 | with: 101 | merge-multiple: true 102 | 103 | - uses: qltysh/qlty-action/coverage@main 104 | with: 105 | token: ${{ secrets.QLTY_COVERAGE_TOKEN }} 106 | files: coverage/*.json 107 | 108 | smoke: 109 | name: Built GEM smoke test 110 | timeout-minutes: 30 111 | runs-on: ubuntu-latest 112 | steps: 113 | - uses: actions/checkout@v4 114 | - name: Set up Ruby 115 | uses: ruby/setup-ruby@v1 116 | with: 117 | ruby-version: ruby 118 | - name: Build GEM 119 | run: gem build 120 | - name: Install built GEM 121 | run: gem install jwt-*.gem 122 | - name: Run test 123 | run: bin/smoke.rb 124 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | jwt.gemspec 3 | pkg 4 | Gemfile.lock 5 | coverage/ 6 | .DS_Store 7 | .rbenv-gemsets 8 | .ruby-version 9 | .vscode/ 10 | .bundle 11 | *gemfile.lock 12 | .byebug_history 13 | *.gem 14 | doc/ 15 | .yardoc/ 16 | -------------------------------------------------------------------------------- /.mdlrc: -------------------------------------------------------------------------------- 1 | rules "~MD013" 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --color 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.5 3 | NewCops: enable 4 | SuggestExtensions: false 5 | Exclude: 6 | - 'gemfiles/*.gemfile' 7 | - 'vendor/**/*' 8 | 9 | Metrics/AbcSize: 10 | Max: 25 11 | 12 | Metrics/MethodLength: 13 | Max: 18 14 | 15 | Metrics/BlockLength: 16 | Exclude: 17 | - spec/**/*_spec.rb 18 | - '*.gemspec' 19 | 20 | Layout/LineLength: 21 | Enabled: false 22 | 23 | Gemspec/DevelopmentDependencies: 24 | EnforcedStyle: gemspec 25 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'openssl' 4 | require 'simplecov_json_formatter' 5 | 6 | SimpleCov.start do 7 | command_name "Job #{File.basename(ENV['BUNDLE_GEMFILE'])}" if ENV['BUNDLE_GEMFILE'] 8 | project_name 'Ruby JWT - Ruby JSON Web Token implementation' 9 | add_filter 'spec' 10 | end 11 | 12 | SimpleCov.formatters = SimpleCov::Formatter::JSONFormatter if ENV['CI'] 13 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --protected 2 | --no-private 3 | - 4 | README.md 5 | CHANGELOG.md 6 | CONTRIBUTING.md 7 | UPGRADING.md 8 | LICENSE 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Tim Rudat 2 | Joakim Antman 3 | Jeff Lindsay 4 | A.B 5 | shields 6 | Bob Aman 7 | Emilio Cristalli 8 | Egon Zemmer 9 | Zane Shannon 10 | Nikita Shatov 11 | Paul Battley 12 | Oliver 13 | blackanger 14 | Ville Lautanala 15 | Tyler Pickett 16 | James Stonehill 17 | Adam Michael 18 | Martin Emde 19 | Saverio Trioni 20 | Peter M. Goldstein 21 | Korstiaan de Ridder 22 | Richard Larocque 23 | Andrew Davis 24 | Yason Khaburzaniya 25 | Klaas Jan Wierenga 26 | Nick Hammond 27 | Bart de Water 28 | Steve Sloan 29 | Antonis Berkakis 30 | Bill Mill 31 | Kevin Olbrich 32 | Simon Fish 33 | jb08 34 | lukas 35 | Rodrigo López Dato 36 | ojab 37 | Ritikesh 38 | sawyerzhang 39 | Larry Lv 40 | smudge 41 | wohlgejm 42 | Tom Wey 43 | yann ARMAND 44 | Brian Flethcer 45 | Jurriaan Pruis 46 | Erik Michaels-Ober 47 | Matthew Simpson 48 | Steven Davidovitz 49 | Nicolas Leger 50 | Pierre Michard 51 | RahulBajaj 52 | Rob Wygand 53 | Ryan Brushett 54 | Ryan McIlmoyl 55 | Ryan Metzler 56 | Severin Schoepke 57 | Shaun Guth 58 | Steve Teti 59 | T.J. Schuck 60 | Taiki Sugawara 61 | Takehiro Adachi 62 | Tobias Haar 63 | Toby Pinder 64 | Tomé Duarte 65 | Travis Hunter 66 | Yuji Yaginuma 67 | Zuzanna Stolińska 68 | aarongray 69 | danielgrippi 70 | fusagiko/takayamaki 71 | mai fujii 72 | nycvotes-dev 73 | revodoge 74 | rono23 75 | antonmorant 76 | Adam Greene 77 | Alexander Boyd 78 | Alexandr Kostrikov 79 | Aman Gupta 80 | Ariel Salomon 81 | Arnaud Mesureur 82 | Artsiom Kuts 83 | Austin Kabiru 84 | B 85 | Bouke van der Bijl 86 | Brandon Keepers 87 | Dan Leyden 88 | Dave Grijalva 89 | Dmitry Pashkevich 90 | Dorian Marié 91 | Ernie Miller 92 | Evgeni Golov 93 | Ewoud Kohl van Wijngaarden 94 | HoneyryderChuck 95 | Igor Victor 96 | Ilyaaaaaaaaaaaaa Zhitomirskiy 97 | Jens Hausherr 98 | Jeremiah Wuenschel 99 | John Downey 100 | Jordan Brough 101 | Josh Bodah 102 | JotaSe 103 | Juanito Fatas 104 | Julio Lopez 105 | Katelyn Kasperowicz 106 | Leonardo Saraiva 107 | Lowell Kirsh 108 | Loïc Lengrand 109 | Lucas Mazza 110 | Makoto Chiba 111 | Manuel Bustillo 112 | Marco Adkins 113 | Meredith Leu 114 | Micah Gates 115 | Michał Begejowicz 116 | Mike Eirih 117 | Mike Pastore 118 | Mingan 119 | Mitch Birti 120 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | appraise 'standalone' do 4 | remove_gem 'rubocop' 5 | end 6 | 7 | appraise 'openssl' do 8 | gem 'openssl', '~> 2.1' 9 | remove_gem 'rubocop' 10 | end 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | 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. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or 22 | advances of any kind 23 | * Trolling, insulting or derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or email 26 | address, without their explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a 28 | professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | 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. 33 | 34 | 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. 35 | 36 | ## Scope 37 | 38 | 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. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at antmanj@gmail.com. All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **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. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **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. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **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. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **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. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 77 | 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). 78 | 79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | For answers to common questions about this code of conduct, see the FAQ at 84 | [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). 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [ruby-jwt](https://github.com/jwt/ruby-jwt) 2 | 3 | ## Forking the project 4 | 5 | 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) 6 | 7 | ``` 8 | git clone git@github.com:you/ruby-jwt.git 9 | cd ruby-jwt 10 | git remote add upstream https://github.com/jwt/ruby-jwt 11 | ``` 12 | 13 | ## Create a branch for your implementation 14 | 15 | Make sure you have the latest upstream main branch of the project. 16 | 17 | ``` 18 | git fetch --all 19 | git checkout main 20 | git rebase upstream/main 21 | git push origin main 22 | git checkout -b fix-a-little-problem 23 | ``` 24 | 25 | ## Running the tests and linter 26 | 27 | Before you start with your implementation make sure you are able to get a successful test run with the current revision. 28 | 29 | 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. 30 | 31 | [Rubocop](https://github.com/rubocop/rubocop) is used to enforce the Ruby style. 32 | 33 | To run the complete set of tests and linter run the following 34 | 35 | ```bash 36 | bundle install 37 | bundle exec appraisal rake test 38 | bundle exec rubocop 39 | ``` 40 | 41 | ## Implement your feature 42 | 43 | Implement tests and your change. Don't be shy adding a little something in the [README](README.md). 44 | Add a short description of the change in either the `Features` or `Fixes` section in the [CHANGELOG](CHANGELOG.md) file. 45 | 46 | The form of the row (You need to return to the row when you know the pull request id) 47 | 48 | ``` 49 | - Fix a little problem [#123](https://github.com/jwt/ruby-jwt/pull/123) - [@you](https://github.com/you). 50 | ``` 51 | 52 | ## Push your branch and create a pull request 53 | 54 | Before pushing make sure the tests pass and RuboCop is happy. 55 | 56 | ``` 57 | bundle exec appraisal rake test 58 | bundle exec rubocop 59 | git push origin fix-a-little-problem 60 | ``` 61 | 62 | 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. 63 | 64 | ## Update the CHANGELOG, again 65 | 66 | Update the [CHANGELOG](CHANGELOG.md) with the pull request id from the previous step. 67 | 68 | You can ammend the previous commit with the updated changelog change and force push your branch. The PR will get automatically updated. 69 | 70 | ``` 71 | git add CHANGELOG.md 72 | git commit --amend --no-edit 73 | git push origin fix-a-little-problem -f 74 | ``` 75 | 76 | ## Keep an eye on your pull request 77 | 78 | A maintainer will review and probably merge you changes when time allows, be patient. 79 | 80 | ## Keeping your branch up-to-date 81 | 82 | It's recommended that you keep your branch up-to-date by rebasing to the upstream main. 83 | 84 | ``` 85 | git fetch upstream 86 | git checkout fix-a-little-problem 87 | git rebase upstream/main 88 | git push origin fix-a-little-problem -f 89 | ``` 90 | 91 | ## Releasing a new version 92 | 93 | The version is using the [Semantic Versioning](http://semver.org/) and the version is located in the [version.rb](lib/jwt/version.rb) file. 94 | Also update the [CHANGELOG](CHANGELOG.md) to reflect the upcoming version release. 95 | 96 | ```bash 97 | rake release 98 | ``` 99 | 100 | **If you want a release cut with your PR, please include a version bump according to ** 101 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Jeff Lindsay 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'bundler/gem_tasks' 5 | 6 | require 'rspec/core/rake_task' 7 | require 'rubocop/rake_task' 8 | 9 | RSpec::Core::RakeTask.new(:test) 10 | RuboCop::RakeTask.new(:rubocop) 11 | 12 | task default: %i[rubocop test] 13 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading ruby-jwt to >= 3.0.0 2 | 3 | ## Removal of the indirect [RbNaCl](https://github.com/RubyCrypto/rbnacl) dependency 4 | 5 | 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. 6 | 7 | Some work to ease the way alternative algorithms can be implemented has been done. This enables the extraction of the algorithm provided by `rbnacl`. 8 | 9 | The extracted algorithms now live in the [jwt-eddsa](https://rubygems.org/gems/jwt-eddsa) gem. 10 | 11 | ### Dropped support for HS512256 12 | 13 | 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. 14 | 15 | ### `JWT::EncodedToken#payload` will raise before token is verified 16 | 17 | 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. 18 | 19 | To access the payload before verification, use the method `JWT::EncodedToken#unverified_payload`. 20 | 21 | ## Stricter requirements on Base64 encoded data 22 | 23 | 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. 24 | 25 | 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. 26 | 27 | ## Claim verification revamp 28 | 29 | 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: 30 | 31 | - The `::JWT::ClaimsValidator` class will be removed in favor of the functionality provided by `::JWT::Claims`. 32 | - The `::JWT::Claims::verify!` method will be removed in favor of `::JWT::Claims::verify_payload!`. 33 | - The `::JWT::JWA.create` method will be removed. 34 | - The `::JWT::Verify` class will be removed in favor of the functionality provided by `::JWT::Claims`. 35 | - Calling `::JWT::Claims::Numeric.new` with a payload will be removed in favor of `::JWT::Claims::verify_payload!(payload, :numeric)`. 36 | - Calling `::JWT::Claims::Numeric.verify!` with a payload will be removed in favor of `::JWT::Claims::verify_payload!(payload, :numeric)`. 37 | 38 | ## Algorithm restructuring 39 | 40 | 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: 41 | 42 | - The `sign` and `verify` static methods on all the algorithms (`::JWT::JWA`) will be removed. 43 | - Custom algorithms are expected to include the `JWT::JWA::SigningAlgorithm` module. 44 | 45 | ## Base64 the `k´ value for HMAC JWKs 46 | 47 | 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. 48 | -------------------------------------------------------------------------------- /bin/console.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'jwt' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/smoke.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'jwt' 5 | 6 | puts "Running simple encode/decode test for #{JWT.gem_version}" 7 | secret = 'secretkeyforsigning' 8 | token = JWT.encode({ con: 'tent' }, secret, 'HS256') 9 | JWT.decode(token, secret, true, algorithm: 'HS256') 10 | -------------------------------------------------------------------------------- /gemfiles/openssl.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "openssl", "~> 2.1" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/standalone.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec path: "../" 6 | -------------------------------------------------------------------------------- /lib/jwt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'jwt/version' 4 | require 'jwt/base64' 5 | require 'jwt/json' 6 | require 'jwt/decode' 7 | require 'jwt/configuration' 8 | require 'jwt/encode' 9 | require 'jwt/error' 10 | require 'jwt/jwk' 11 | require 'jwt/claims' 12 | require 'jwt/encoded_token' 13 | require 'jwt/token' 14 | 15 | # JSON Web Token implementation 16 | # 17 | # Should be up to date with the latest spec: 18 | # https://tools.ietf.org/html/rfc7519 19 | module JWT 20 | extend ::JWT::Configuration 21 | 22 | module_function 23 | 24 | # Encodes a payload into a JWT. 25 | # 26 | # @param payload [Hash] the payload to encode. 27 | # @param key [String] the key used to sign the JWT. 28 | # @param algorithm [String] the algorithm used to sign the JWT. 29 | # @param header_fields [Hash] additional headers to include in the JWT. 30 | # @return [String] the encoded JWT. 31 | def encode(payload, key, algorithm = 'HS256', header_fields = {}) 32 | Encode.new(payload: payload, 33 | key: key, 34 | algorithm: algorithm, 35 | headers: header_fields).segments 36 | end 37 | 38 | # Decodes a JWT to extract the payload and header 39 | # 40 | # @param jwt [String] the JWT to decode. 41 | # @param key [String] the key used to verify the JWT. 42 | # @param verify [Boolean] whether to verify the JWT signature. 43 | # @param options [Hash] additional options for decoding. 44 | # @return [Array] the decoded payload and headers. 45 | def decode(jwt, key = nil, verify = true, options = {}, &keyfinder) # rubocop:disable Style/OptionalBooleanParameter 46 | Decode.new(jwt, key, verify, configuration.decode.to_h.merge(options), &keyfinder).decode_segments 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/jwt/base64.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'base64' 4 | 5 | module JWT 6 | # Base64 encoding and decoding 7 | # @api private 8 | class Base64 9 | class << self 10 | # Encode a string with URL-safe Base64 complying with RFC 4648 (not padded). 11 | # @api private 12 | def url_encode(str) 13 | ::Base64.urlsafe_encode64(str, padding: false) 14 | end 15 | 16 | # Decode a string with URL-safe Base64 complying with RFC 4648. 17 | # @api private 18 | def url_decode(str) 19 | ::Base64.urlsafe_decode64(str) 20 | rescue ArgumentError => e 21 | raise unless e.message == 'invalid base64' 22 | 23 | raise Base64DecodeError, 'Invalid base64 encoding' 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/jwt/claims.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'claims/audience' 4 | require_relative 'claims/crit' 5 | require_relative 'claims/decode_verifier' 6 | require_relative 'claims/expiration' 7 | require_relative 'claims/issued_at' 8 | require_relative 'claims/issuer' 9 | require_relative 'claims/jwt_id' 10 | require_relative 'claims/not_before' 11 | require_relative 'claims/numeric' 12 | require_relative 'claims/required' 13 | require_relative 'claims/subject' 14 | require_relative 'claims/verifier' 15 | 16 | module JWT 17 | # JWT Claim verifications 18 | # https://datatracker.ietf.org/doc/html/rfc7519#section-4 19 | # 20 | # Verification is supported for the following claims: 21 | # exp 22 | # nbf 23 | # iss 24 | # iat 25 | # jti 26 | # aud 27 | # sub 28 | # required 29 | # numeric 30 | module Claims 31 | # Represents a claim verification error 32 | Error = Struct.new(:message, keyword_init: true) 33 | 34 | class << self 35 | # Checks if the claims in the JWT payload are valid. 36 | # @example 37 | # 38 | # ::JWT::Claims.verify_payload!({"exp" => Time.now.to_i + 10}, :exp) 39 | # ::JWT::Claims.verify_payload!({"exp" => Time.now.to_i - 10}, exp: { leeway: 11}) 40 | # 41 | # @param payload [Hash] the JWT payload. 42 | # @param options [Array] the options for verifying the claims. 43 | # @return [void] 44 | # @raise [JWT::DecodeError] if any claim is invalid. 45 | def verify_payload!(payload, *options) 46 | Verifier.verify!(VerificationContext.new(payload: payload), *options) 47 | end 48 | 49 | # Checks if the claims in the JWT payload are valid. 50 | # 51 | # @param payload [Hash] the JWT payload. 52 | # @param options [Array] the options for verifying the claims. 53 | # @return [Boolean] true if the claims are valid, false otherwise 54 | def valid_payload?(payload, *options) 55 | payload_errors(payload, *options).empty? 56 | end 57 | 58 | # Returns the errors in the claims of the JWT token. 59 | # 60 | # @param options [Array] the options for verifying the claims. 61 | # @return [Array] the errors in the claims of the JWT 62 | def payload_errors(payload, *options) 63 | Verifier.errors(VerificationContext.new(payload: payload), *options) 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/jwt/claims/audience.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module Claims 5 | # The Audience class is responsible for validating the audience claim ('aud') in a JWT token. 6 | class Audience 7 | # Initializes a new Audience instance. 8 | # 9 | # @param expected_audience [String, Array] the expected audience(s) for the JWT token. 10 | def initialize(expected_audience:) 11 | @expected_audience = expected_audience 12 | end 13 | 14 | # Verifies the audience claim ('aud') in the JWT token. 15 | # 16 | # @param context [Object] the context containing the JWT payload. 17 | # @param _args [Hash] additional arguments (not used). 18 | # @raise [JWT::InvalidAudError] if the audience claim is invalid. 19 | # @return [nil] 20 | def verify!(context:, **_args) 21 | aud = context.payload['aud'] 22 | raise JWT::InvalidAudError, "Invalid audience. Expected #{expected_audience}, received #{aud || ''}" if ([*aud] & [*expected_audience]).empty? 23 | end 24 | 25 | private 26 | 27 | attr_reader :expected_audience 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/jwt/claims/crit.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module Claims 5 | # Responsible of validation the crit header 6 | class Crit 7 | # Initializes a new Crit instance. 8 | # 9 | # @param expected_crits [String] the expected crit header values for the JWT token. 10 | def initialize(expected_crits:) 11 | @expected_crits = Array(expected_crits) 12 | end 13 | 14 | # Verifies the critical claim ('crit') in the JWT token header. 15 | # 16 | # @param context [Object] the context containing the JWT payload and header. 17 | # @param _args [Hash] additional arguments (not used). 18 | # @raise [JWT::InvalidCritError] if the crit claim is invalid. 19 | # @return [nil] 20 | def verify!(context:, **_args) 21 | raise(JWT::InvalidCritError, 'Crit header missing') unless context.header['crit'] 22 | raise(JWT::InvalidCritError, 'Crit header should be an array') unless context.header['crit'].is_a?(Array) 23 | 24 | missing = (expected_crits - context.header['crit']) 25 | raise(JWT::InvalidCritError, "Crit header missing expected values: #{missing.join(', ')}") if missing.any? 26 | 27 | nil 28 | end 29 | 30 | private 31 | 32 | attr_reader :expected_crits 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/jwt/claims/decode_verifier.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module Claims 5 | # Context class to contain the data passed to individual claim validators 6 | # 7 | # @api private 8 | VerificationContext = Struct.new(:payload, keyword_init: true) 9 | 10 | # Verifiers to support the ::JWT.decode method 11 | # 12 | # @api private 13 | module DecodeVerifier 14 | VERIFIERS = { 15 | verify_expiration: ->(options) { Claims::Expiration.new(leeway: options[:exp_leeway] || options[:leeway]) }, 16 | verify_not_before: ->(options) { Claims::NotBefore.new(leeway: options[:nbf_leeway] || options[:leeway]) }, 17 | verify_iss: ->(options) { options[:iss] && Claims::Issuer.new(issuers: options[:iss]) }, 18 | verify_iat: ->(*) { Claims::IssuedAt.new }, 19 | verify_jti: ->(options) { Claims::JwtId.new(validator: options[:verify_jti]) }, 20 | verify_aud: ->(options) { options[:aud] && Claims::Audience.new(expected_audience: options[:aud]) }, 21 | verify_sub: ->(options) { options[:sub] && Claims::Subject.new(expected_subject: options[:sub]) }, 22 | required_claims: ->(options) { Claims::Required.new(required_claims: options[:required_claims]) } 23 | }.freeze 24 | 25 | private_constant(:VERIFIERS) 26 | 27 | class << self 28 | # @api private 29 | def verify!(payload, options) 30 | VERIFIERS.each do |key, verifier_builder| 31 | next unless options[key] || options[key.to_s] 32 | 33 | verifier_builder&.call(options)&.verify!(context: VerificationContext.new(payload: payload)) 34 | end 35 | nil 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/jwt/claims/expiration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module Claims 5 | # The Expiration class is responsible for validating the expiration claim ('exp') in a JWT token. 6 | class Expiration 7 | # Initializes a new Expiration instance. 8 | # 9 | # @param leeway [Integer] the amount of leeway (in seconds) to allow when validating the expiration time. Default: 0. 10 | def initialize(leeway:) 11 | @leeway = leeway || 0 12 | end 13 | 14 | # Verifies the expiration claim ('exp') in the JWT token. 15 | # 16 | # @param context [Object] the context containing the JWT payload. 17 | # @param _args [Hash] additional arguments (not used). 18 | # @raise [JWT::ExpiredSignature] if the token has expired. 19 | # @return [nil] 20 | def verify!(context:, **_args) 21 | return unless context.payload.is_a?(Hash) 22 | return unless context.payload.key?('exp') 23 | 24 | raise JWT::ExpiredSignature, 'Signature has expired' if context.payload['exp'].to_i <= (Time.now.to_i - leeway) 25 | end 26 | 27 | private 28 | 29 | attr_reader :leeway 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/jwt/claims/issued_at.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module Claims 5 | # The IssuedAt class is responsible for validating the issued at claim ('iat') in a JWT token. 6 | class IssuedAt 7 | # Verifies the issued at claim ('iat') in the JWT token. 8 | # 9 | # @param context [Object] the context containing the JWT payload. 10 | # @param _args [Hash] additional arguments (not used). 11 | # @raise [JWT::InvalidIatError] if the issued at claim is invalid. 12 | # @return [nil] 13 | def verify!(context:, **_args) 14 | return unless context.payload.is_a?(Hash) 15 | return unless context.payload.key?('iat') 16 | 17 | iat = context.payload['iat'] 18 | raise(JWT::InvalidIatError, 'Invalid iat') if !iat.is_a?(::Numeric) || iat.to_f > Time.now.to_f 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/jwt/claims/issuer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module Claims 5 | # The Issuer class is responsible for validating the issuer claim ('iss') in a JWT token. 6 | class Issuer 7 | # Initializes a new Issuer instance. 8 | # 9 | # @param issuers [String, Symbol, Array] the expected issuer(s) for the JWT token. 10 | def initialize(issuers:) 11 | @issuers = Array(issuers).map { |item| item.is_a?(Symbol) ? item.to_s : item } 12 | end 13 | 14 | # Verifies the issuer claim ('iss') in the JWT token. 15 | # 16 | # @param context [Object] the context containing the JWT payload. 17 | # @param _args [Hash] additional arguments (not used). 18 | # @raise [JWT::InvalidIssuerError] if the issuer claim is invalid. 19 | # @return [nil] 20 | def verify!(context:, **_args) 21 | case (iss = context.payload['iss']) 22 | when *issuers 23 | nil 24 | else 25 | raise JWT::InvalidIssuerError, "Invalid issuer. Expected #{issuers}, received #{iss || ''}" 26 | end 27 | end 28 | 29 | private 30 | 31 | attr_reader :issuers 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/jwt/claims/jwt_id.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module Claims 5 | # The JwtId class is responsible for validating the JWT ID claim ('jti') in a JWT token. 6 | class JwtId 7 | # Initializes a new JwtId instance. 8 | # 9 | # @param validator [#call] an object responding to `call` to validate the JWT ID. 10 | def initialize(validator:) 11 | @validator = validator 12 | end 13 | 14 | # Verifies the JWT ID claim ('jti') in the JWT token. 15 | # 16 | # @param context [Object] the context containing the JWT payload. 17 | # @param _args [Hash] additional arguments (not used). 18 | # @raise [JWT::InvalidJtiError] if the JWT ID claim is invalid or missing. 19 | # @return [nil] 20 | def verify!(context:, **_args) 21 | jti = context.payload['jti'] 22 | if validator.respond_to?(:call) 23 | verified = validator.arity == 2 ? validator.call(jti, context.payload) : validator.call(jti) 24 | raise(JWT::InvalidJtiError, 'Invalid jti') unless verified 25 | elsif jti.to_s.strip.empty? 26 | raise(JWT::InvalidJtiError, 'Missing jti') 27 | end 28 | end 29 | 30 | private 31 | 32 | attr_reader :validator 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/jwt/claims/not_before.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module Claims 5 | # The NotBefore class is responsible for validating the 'nbf' (Not Before) claim in a JWT token. 6 | class NotBefore 7 | # Initializes a new NotBefore instance. 8 | # 9 | # @param leeway [Integer] the amount of leeway (in seconds) to allow when validating the 'nbf' claim. Defaults to 0. 10 | def initialize(leeway:) 11 | @leeway = leeway || 0 12 | end 13 | 14 | # Verifies the 'nbf' (Not Before) claim in the JWT token. 15 | # 16 | # @param context [Object] the context containing the JWT payload. 17 | # @param _args [Hash] additional arguments (not used). 18 | # @raise [JWT::ImmatureSignature] if the 'nbf' claim has not been reached. 19 | # @return [nil] 20 | def verify!(context:, **_args) 21 | return unless context.payload.is_a?(Hash) 22 | return unless context.payload.key?('nbf') 23 | 24 | raise JWT::ImmatureSignature, 'Signature nbf has not been reached' if context.payload['nbf'].to_i > (Time.now.to_i + leeway) 25 | end 26 | 27 | private 28 | 29 | attr_reader :leeway 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/jwt/claims/numeric.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module Claims 5 | # The Numeric class is responsible for validating numeric claims in a JWT token. 6 | # The numeric claims are: exp, iat and nbf 7 | class Numeric 8 | # List of numeric claims that can be validated. 9 | NUMERIC_CLAIMS = %i[ 10 | exp 11 | iat 12 | nbf 13 | ].freeze 14 | 15 | private_constant(:NUMERIC_CLAIMS) 16 | 17 | # Verifies the numeric claims in the JWT context. 18 | # 19 | # @param context [Object] the context containing the JWT payload. 20 | # @raise [JWT::InvalidClaimError] if any numeric claim is invalid. 21 | # @return [nil] 22 | def verify!(context:) 23 | validate_numeric_claims(context.payload) 24 | end 25 | 26 | private 27 | 28 | def validate_numeric_claims(payload) 29 | NUMERIC_CLAIMS.each do |claim| 30 | validate_is_numeric(payload, claim) 31 | end 32 | end 33 | 34 | def validate_is_numeric(payload, claim) 35 | return unless payload.is_a?(Hash) 36 | return unless payload.key?(claim) || 37 | payload.key?(claim.to_s) 38 | 39 | return if payload[claim].is_a?(::Numeric) || payload[claim.to_s].is_a?(::Numeric) 40 | 41 | raise InvalidPayload, "#{claim} claim must be a Numeric value but it is a #{(payload[claim] || payload[claim.to_s]).class}" 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/jwt/claims/required.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module Claims 5 | # The Required class is responsible for validating that all required claims are present in a JWT token. 6 | class Required 7 | # Initializes a new Required instance. 8 | # 9 | # @param required_claims [Array] the list of required claims. 10 | def initialize(required_claims:) 11 | @required_claims = required_claims 12 | end 13 | 14 | # Verifies that all required claims are present in the JWT payload. 15 | # 16 | # @param context [Object] the context containing the JWT payload. 17 | # @param _args [Hash] additional arguments (not used). 18 | # @raise [JWT::MissingRequiredClaim] if any required claim is missing. 19 | # @return [nil] 20 | def verify!(context:, **_args) 21 | required_claims.each do |required_claim| 22 | next if context.payload.is_a?(Hash) && context.payload.key?(required_claim) 23 | 24 | raise JWT::MissingRequiredClaim, "Missing required claim #{required_claim}" 25 | end 26 | end 27 | 28 | private 29 | 30 | attr_reader :required_claims 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/jwt/claims/subject.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module Claims 5 | # The Subject class is responsible for validating the subject claim ('sub') in a JWT token. 6 | class Subject 7 | # Initializes a new Subject instance. 8 | # 9 | # @param expected_subject [String] the expected subject for the JWT token. 10 | def initialize(expected_subject:) 11 | @expected_subject = expected_subject.to_s 12 | end 13 | 14 | # Verifies the subject claim ('sub') in the JWT token. 15 | # 16 | # @param context [Object] the context containing the JWT payload. 17 | # @param _args [Hash] additional arguments (not used). 18 | # @raise [JWT::InvalidSubError] if the subject claim is invalid. 19 | # @return [nil] 20 | def verify!(context:, **_args) 21 | sub = context.payload['sub'] 22 | raise(JWT::InvalidSubError, "Invalid subject. Expected #{expected_subject}, received #{sub || ''}") unless sub.to_s == expected_subject 23 | end 24 | 25 | private 26 | 27 | attr_reader :expected_subject 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/jwt/claims/verifier.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module Claims 5 | # @api private 6 | module Verifier 7 | VERIFIERS = { 8 | exp: ->(options) { Claims::Expiration.new(leeway: options.dig(:exp, :leeway)) }, 9 | nbf: ->(options) { Claims::NotBefore.new(leeway: options.dig(:nbf, :leeway)) }, 10 | iss: ->(options) { Claims::Issuer.new(issuers: options[:iss]) }, 11 | iat: ->(*) { Claims::IssuedAt.new }, 12 | jti: ->(options) { Claims::JwtId.new(validator: options[:jti]) }, 13 | aud: ->(options) { Claims::Audience.new(expected_audience: options[:aud]) }, 14 | sub: ->(options) { Claims::Subject.new(expected_subject: options[:sub]) }, 15 | crit: ->(options) { Claims::Crit.new(expected_crits: options[:crit]) }, 16 | required: ->(options) { Claims::Required.new(required_claims: options[:required]) }, 17 | numeric: ->(*) { Claims::Numeric.new } 18 | }.freeze 19 | 20 | private_constant(:VERIFIERS) 21 | 22 | class << self 23 | # @api private 24 | def verify!(context, *options) 25 | iterate_verifiers(*options) do |verifier, verifier_options| 26 | verify_one!(context, verifier, verifier_options) 27 | end 28 | nil 29 | end 30 | 31 | # @api private 32 | def errors(context, *options) 33 | errors = [] 34 | iterate_verifiers(*options) do |verifier, verifier_options| 35 | verify_one!(context, verifier, verifier_options) 36 | rescue ::JWT::DecodeError => e 37 | errors << Error.new(message: e.message) 38 | end 39 | errors 40 | end 41 | 42 | private 43 | 44 | def iterate_verifiers(*options) 45 | options.each do |element| 46 | if element.is_a?(Hash) 47 | element.each_key { |key| yield(key, element) } 48 | else 49 | yield(element, {}) 50 | end 51 | end 52 | end 53 | 54 | def verify_one!(context, verifier, options) 55 | verifier_builder = VERIFIERS.fetch(verifier) { raise ArgumentError, "#{verifier} not a valid claim verifier" } 56 | verifier_builder.call(options || {}).verify!(context: context) 57 | end 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/jwt/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'configuration/container' 4 | 5 | module JWT 6 | # The Configuration module provides methods to configure JWT settings. 7 | module Configuration 8 | # Configures the JWT settings. 9 | # 10 | # @yield [config] Gives the current configuration to the block. 11 | # @yieldparam config [JWT::Configuration::Container] the configuration container. 12 | def configure 13 | yield(configuration) 14 | end 15 | 16 | # Returns the JWT configuration container. 17 | # 18 | # @return [JWT::Configuration::Container] the configuration container. 19 | def configuration 20 | @configuration ||= ::JWT::Configuration::Container.new 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/jwt/configuration/container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'decode_configuration' 4 | require_relative 'jwk_configuration' 5 | 6 | module JWT 7 | module Configuration 8 | # The Container class holds the configuration settings for JWT. 9 | class Container 10 | # @!attribute [rw] decode 11 | # @return [DecodeConfiguration] the decode configuration. 12 | # @!attribute [rw] jwk 13 | # @return [JwkConfiguration] the JWK configuration. 14 | # @!attribute [rw] strict_base64_decoding 15 | # @return [Boolean] whether strict Base64 decoding is enabled. 16 | attr_accessor :decode, :jwk, :strict_base64_decoding 17 | 18 | # @!attribute [r] deprecation_warnings 19 | # @return [Symbol] the deprecation warnings setting. 20 | attr_reader :deprecation_warnings 21 | 22 | # Initializes a new Container instance and resets the configuration. 23 | def initialize 24 | reset! 25 | end 26 | 27 | # Resets the configuration to default values. 28 | # 29 | # @return [void] 30 | def reset! 31 | @decode = DecodeConfiguration.new 32 | @jwk = JwkConfiguration.new 33 | 34 | self.deprecation_warnings = :once 35 | end 36 | 37 | DEPRECATION_WARNINGS_VALUES = %i[once warn silent].freeze 38 | private_constant(:DEPRECATION_WARNINGS_VALUES) 39 | # Sets the deprecation warnings setting. 40 | # 41 | # @param value [Symbol] the deprecation warnings setting. Must be one of `:once`, `:warn`, or `:silent`. 42 | # @raise [ArgumentError] if the value is not one of the supported values. 43 | # @return [void] 44 | def deprecation_warnings=(value) 45 | raise ArgumentError, "Invalid deprecation_warnings value #{value}. Supported values: #{DEPRECATION_WARNINGS_VALUES}" unless DEPRECATION_WARNINGS_VALUES.include?(value) 46 | 47 | @deprecation_warnings = value 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/jwt/configuration/decode_configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module Configuration 5 | # The DecodeConfiguration class holds the configuration settings for decoding JWT tokens. 6 | class DecodeConfiguration 7 | # @!attribute [rw] verify_expiration 8 | # @return [Boolean] whether to verify the expiration claim. 9 | # @!attribute [rw] verify_not_before 10 | # @return [Boolean] whether to verify the not before claim. 11 | # @!attribute [rw] verify_iss 12 | # @return [Boolean] whether to verify the issuer claim. 13 | # @!attribute [rw] verify_iat 14 | # @return [Boolean] whether to verify the issued at claim. 15 | # @!attribute [rw] verify_jti 16 | # @return [Boolean] whether to verify the JWT ID claim. 17 | # @!attribute [rw] verify_aud 18 | # @return [Boolean] whether to verify the audience claim. 19 | # @!attribute [rw] verify_sub 20 | # @return [Boolean] whether to verify the subject claim. 21 | # @!attribute [rw] leeway 22 | # @return [Integer] the leeway in seconds for time-based claims. 23 | # @!attribute [rw] algorithms 24 | # @return [Array] the list of acceptable algorithms. 25 | # @!attribute [rw] required_claims 26 | # @return [Array] the list of required claims. 27 | 28 | attr_accessor :verify_expiration, 29 | :verify_not_before, 30 | :verify_iss, 31 | :verify_iat, 32 | :verify_jti, 33 | :verify_aud, 34 | :verify_sub, 35 | :leeway, 36 | :algorithms, 37 | :required_claims 38 | 39 | # Initializes a new DecodeConfiguration instance with default settings. 40 | def initialize 41 | @verify_expiration = true 42 | @verify_not_before = true 43 | @verify_iss = false 44 | @verify_iat = false 45 | @verify_jti = false 46 | @verify_aud = false 47 | @verify_sub = false 48 | @leeway = 0 49 | @algorithms = ['HS256'] 50 | @required_claims = [] 51 | end 52 | 53 | # @api private 54 | def to_h 55 | { 56 | verify_expiration: verify_expiration, 57 | verify_not_before: verify_not_before, 58 | verify_iss: verify_iss, 59 | verify_iat: verify_iat, 60 | verify_jti: verify_jti, 61 | verify_aud: verify_aud, 62 | verify_sub: verify_sub, 63 | leeway: leeway, 64 | algorithms: algorithms, 65 | required_claims: required_claims 66 | } 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/jwt/configuration/jwk_configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../jwk/kid_as_key_digest' 4 | require_relative '../jwk/thumbprint' 5 | 6 | module JWT 7 | module Configuration 8 | # @api private 9 | class JwkConfiguration 10 | def initialize 11 | self.kid_generator_type = :key_digest 12 | end 13 | 14 | def kid_generator_type=(value) 15 | self.kid_generator = case value 16 | when :key_digest 17 | JWT::JWK::KidAsKeyDigest 18 | when :rfc7638_thumbprint 19 | JWT::JWK::Thumbprint 20 | else 21 | raise ArgumentError, "#{value} is not a valid kid generator type." 22 | end 23 | end 24 | 25 | attr_accessor :kid_generator 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/jwt/decode.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | require 'jwt/x5c_key_finder' 5 | 6 | module JWT 7 | # The Decode class is responsible for decoding and verifying JWT tokens. 8 | class Decode 9 | # Order is very important - first check for string keys, next for symbols 10 | ALGORITHM_KEYS = ['algorithm', 11 | :algorithm, 12 | 'algorithms', 13 | :algorithms].freeze 14 | # Initializes a new Decode instance. 15 | # 16 | # @param jwt [String] the JWT to decode. 17 | # @param key [String, Array] the key(s) to use for verification. 18 | # @param verify [Boolean] whether to verify the token's signature. 19 | # @param options [Hash] additional options for decoding and verification. 20 | # @param keyfinder [Proc] an optional key finder block to dynamically find the key for verification. 21 | # @raise [JWT::DecodeError] if decoding or verification fails. 22 | def initialize(jwt, key, verify, options, &keyfinder) 23 | raise JWT::DecodeError, 'Nil JSON web token' unless jwt 24 | 25 | @token = EncodedToken.new(jwt) 26 | @key = key 27 | @options = options 28 | @verify = verify 29 | @keyfinder = keyfinder 30 | end 31 | 32 | # Decodes the JWT token and verifies its segments if verification is enabled. 33 | # 34 | # @return [Array] an array containing the decoded payload and header. 35 | def decode_segments 36 | validate_segment_count! 37 | if @verify 38 | verify_algo 39 | set_key 40 | verify_signature 41 | Claims::DecodeVerifier.verify!(token.unverified_payload, @options) 42 | end 43 | 44 | [token.unverified_payload, token.header] 45 | end 46 | 47 | private 48 | 49 | attr_reader :token 50 | 51 | def verify_signature 52 | return if none_algorithm? 53 | 54 | raise JWT::DecodeError, 'No verification key available' unless @key 55 | 56 | token.verify_signature!(algorithm: allowed_and_valid_algorithms, key: @key) 57 | end 58 | 59 | def verify_algo 60 | raise JWT::IncorrectAlgorithm, 'An algorithm must be specified' if allowed_algorithms.empty? 61 | raise JWT::DecodeError, 'Token header not a JSON object' unless token.header.is_a?(Hash) 62 | raise JWT::IncorrectAlgorithm, 'Token is missing alg header' unless alg_in_header 63 | raise JWT::IncorrectAlgorithm, 'Expected a different algorithm' if allowed_and_valid_algorithms.empty? 64 | end 65 | 66 | def set_key 67 | @key = find_key(&@keyfinder) if @keyfinder 68 | @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid]).key_for(token.header['kid']) if @options[:jwks] 69 | return unless (x5c_options = @options[:x5c]) 70 | 71 | @key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(token.header['x5c']) 72 | end 73 | 74 | def allowed_and_valid_algorithms 75 | @allowed_and_valid_algorithms ||= allowed_algorithms.select { |alg| alg.valid_alg?(alg_in_header) } 76 | end 77 | 78 | def given_algorithms 79 | alg_key = ALGORITHM_KEYS.find { |key| @options[key] } 80 | Array(@options[alg_key]) 81 | end 82 | 83 | def allowed_algorithms 84 | @allowed_algorithms ||= resolve_allowed_algorithms 85 | end 86 | 87 | def resolve_allowed_algorithms 88 | given_algorithms.map { |alg| JWA.resolve(alg) } 89 | end 90 | 91 | def find_key(&keyfinder) 92 | key = (keyfinder.arity == 2 ? yield(token.header, token.unverified_payload) : yield(token.header)) 93 | # key can be of type [string, nil, OpenSSL::PKey, Array] 94 | return key if key && !Array(key).empty? 95 | 96 | raise JWT::DecodeError, 'No verification key available' 97 | end 98 | 99 | def validate_segment_count! 100 | segment_count = token.jwt.count('.') + 1 101 | return if segment_count == 3 102 | return if !@verify && segment_count == 2 # If no verifying required, the signature is not needed 103 | return if segment_count == 2 && none_algorithm? 104 | 105 | raise JWT::DecodeError, 'Not enough or too many segments' 106 | end 107 | 108 | def none_algorithm? 109 | alg_in_header == 'none' 110 | end 111 | 112 | def alg_in_header 113 | token.header['alg'] 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/jwt/encode.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'jwa' 4 | 5 | module JWT 6 | # The Encode class is responsible for encoding JWT tokens. 7 | class Encode 8 | # Initializes a new Encode instance. 9 | # 10 | # @param options [Hash] the options for encoding the JWT token. 11 | # @option options [Hash] :payload the payload of the JWT token. 12 | # @option options [Hash] :headers the headers of the JWT token. 13 | # @option options [String] :key the key used to sign the JWT token. 14 | # @option options [String] :algorithm the algorithm used to sign the JWT token. 15 | def initialize(options) 16 | @token = Token.new(payload: options[:payload], header: options[:headers]) 17 | @key = options[:key] 18 | @algorithm = options[:algorithm] 19 | end 20 | 21 | # Encodes the JWT token and returns its segments. 22 | # 23 | # @return [String] the encoded JWT token. 24 | def segments 25 | @token.verify_claims!(:numeric) 26 | @token.sign!(algorithm: @algorithm, key: @key) 27 | @token.jwt 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/jwt/encoded_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | # Represents an encoded JWT token 5 | # 6 | # Processing an encoded and signed token: 7 | # 8 | # token = JWT::Token.new(payload: {pay: 'load'}) 9 | # token.sign!(algorithm: 'HS256', key: 'secret') 10 | # 11 | # encoded_token = JWT::EncodedToken.new(token.jwt) 12 | # encoded_token.verify_signature!(algorithm: 'HS256', key: 'secret') 13 | # encoded_token.payload # => {'pay' => 'load'} 14 | class EncodedToken 15 | # @private 16 | # Allow access to the unverified payload for claim verification. 17 | class ClaimsContext 18 | extend Forwardable 19 | 20 | def_delegators :@token, :header, :unverified_payload 21 | 22 | def initialize(token) 23 | @token = token 24 | end 25 | 26 | def payload 27 | unverified_payload 28 | end 29 | end 30 | 31 | # Returns the original token provided to the class. 32 | # @return [String] The JWT token. 33 | attr_reader :jwt 34 | 35 | # Initializes a new EncodedToken instance. 36 | # 37 | # @param jwt [String] the encoded JWT token. 38 | # @raise [ArgumentError] if the provided JWT is not a String. 39 | def initialize(jwt) 40 | raise ArgumentError, 'Provided JWT must be a String' unless jwt.is_a?(String) 41 | 42 | @jwt = jwt 43 | @signature_verified = false 44 | @encoded_header, @encoded_payload, @encoded_signature = jwt.split('.') 45 | end 46 | 47 | # Returns the decoded signature of the JWT token. 48 | # 49 | # @return [String] the decoded signature. 50 | def signature 51 | @signature ||= ::JWT::Base64.url_decode(encoded_signature || '') 52 | end 53 | 54 | # Returns the encoded signature of the JWT token. 55 | # 56 | # @return [String] the encoded signature. 57 | attr_reader :encoded_signature 58 | 59 | # Returns the decoded header of the JWT token. 60 | # 61 | # @return [Hash] the header. 62 | def header 63 | @header ||= parse_and_decode(@encoded_header) 64 | end 65 | 66 | # Returns the encoded header of the JWT token. 67 | # 68 | # @return [String] the encoded header. 69 | attr_reader :encoded_header 70 | 71 | # Returns the payload of the JWT token. Access requires the signature to have been verified. 72 | # 73 | # @return [Hash] the payload. 74 | # @raise [JWT::DecodeError] if the signature has not been verified. 75 | def payload 76 | raise JWT::DecodeError, 'Verify the token signature before accessing the payload' unless @signature_verified 77 | 78 | decoded_payload 79 | end 80 | 81 | # Returns the payload of the JWT token without requiring the signature to have been verified. 82 | # @return [Hash] the payload. 83 | def unverified_payload 84 | decoded_payload 85 | end 86 | 87 | # Sets or returns the encoded payload of the JWT token. 88 | # 89 | # @return [String] the encoded payload. 90 | attr_accessor :encoded_payload 91 | 92 | # Returns the signing input of the JWT token. 93 | # 94 | # @return [String] the signing input. 95 | def signing_input 96 | [encoded_header, encoded_payload].join('.') 97 | end 98 | 99 | # Verifies the token signature and claims. 100 | # By default it verifies the 'exp' claim. 101 | # 102 | # @example 103 | # encoded_token.verify!(signature: { algorithm: 'HS256', key: 'secret' }, claims: [:exp]) 104 | # 105 | # @param signature [Hash] the parameters for signature verification (see {#verify_signature!}). 106 | # @param claims [Array, Hash] the claims to verify (see {#verify_claims!}). 107 | # @return [nil] 108 | # @raise [JWT::DecodeError] if the signature or claim verification fails. 109 | def verify!(signature:, claims: [:exp]) 110 | verify_signature!(**signature) 111 | claims.is_a?(Array) ? verify_claims!(*claims) : verify_claims!(claims) 112 | nil 113 | end 114 | 115 | # Verifies the signature of the JWT token. 116 | # 117 | # @param algorithm [String, Array, Object, Array] the algorithm(s) to use for verification. 118 | # @param key [String, Array] the key(s) to use for verification. 119 | # @param key_finder [#call] an object responding to `call` to find the key for verification. 120 | # @return [nil] 121 | # @raise [JWT::VerificationError] if the signature verification fails. 122 | # @raise [ArgumentError] if neither key nor key_finder is provided, or if both are provided. 123 | def verify_signature!(algorithm:, key: nil, key_finder: nil) 124 | raise ArgumentError, 'Provide either key or key_finder, not both or neither' if key.nil? == key_finder.nil? 125 | 126 | key ||= key_finder.call(self) 127 | 128 | return if valid_signature?(algorithm: algorithm, key: key) 129 | 130 | raise JWT::VerificationError, 'Signature verification failed' 131 | end 132 | 133 | # Checks if the signature of the JWT token is valid. 134 | # 135 | # @param algorithm [String, Array, Object, Array] the algorithm(s) to use for verification. 136 | # @param key [String, Array] the key(s) to use for verification. 137 | # @return [Boolean] true if the signature is valid, false otherwise. 138 | def valid_signature?(algorithm:, key:) 139 | valid = Array(JWA.resolve_and_sort(algorithms: algorithm, preferred_algorithm: header['alg'])).any? do |algo| 140 | Array(key).any? do |one_key| 141 | algo.verify(data: signing_input, signature: signature, verification_key: one_key) 142 | end 143 | end 144 | 145 | valid.tap { |verified| @signature_verified = verified } 146 | end 147 | 148 | # Verifies the claims of the token. 149 | # @param options [Array, Hash] the claims to verify. 150 | # @raise [JWT::DecodeError] if the claims are invalid. 151 | def verify_claims!(*options) 152 | Claims::Verifier.verify!(ClaimsContext.new(self), *options) 153 | end 154 | 155 | # Returns the errors of the claims of the token. 156 | # @param options [Array, Hash] the claims to verify. 157 | # @return [Array] the errors of the claims. 158 | def claim_errors(*options) 159 | Claims::Verifier.errors(ClaimsContext.new(self), *options) 160 | end 161 | 162 | # Returns whether the claims of the token are valid. 163 | # @param options [Array, Hash] the claims to verify. 164 | # @return [Boolean] whether the claims are valid. 165 | def valid_claims?(*options) 166 | claim_errors(*options).empty? 167 | end 168 | 169 | alias to_s jwt 170 | 171 | private 172 | 173 | def decode_payload 174 | raise JWT::DecodeError, 'Encoded payload is empty' if encoded_payload == '' 175 | 176 | if unencoded_payload? 177 | verify_claims!(crit: ['b64']) 178 | return parse_unencoded(encoded_payload) 179 | end 180 | 181 | parse_and_decode(encoded_payload) 182 | end 183 | 184 | def unencoded_payload? 185 | header['b64'] == false 186 | end 187 | 188 | def parse_and_decode(segment) 189 | parse(::JWT::Base64.url_decode(segment || '')) 190 | end 191 | 192 | def parse_unencoded(segment) 193 | parse(segment) 194 | end 195 | 196 | def parse(segment) 197 | JWT::JSON.parse(segment) 198 | rescue ::JSON::ParserError 199 | raise JWT::DecodeError, 'Invalid segment encoding' 200 | end 201 | 202 | def decoded_payload 203 | @decoded_payload ||= decode_payload 204 | end 205 | end 206 | end 207 | -------------------------------------------------------------------------------- /lib/jwt/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | # The EncodeError class is raised when there is an error encoding a JWT. 5 | class EncodeError < StandardError; end 6 | 7 | # The DecodeError class is raised when there is an error decoding a JWT. 8 | class DecodeError < StandardError; end 9 | 10 | # The RequiredDependencyError class is raised when a required dependency is missing. 11 | class RequiredDependencyError < StandardError; end 12 | 13 | # The VerificationError class is raised when there is an error verifying a JWT. 14 | class VerificationError < DecodeError; end 15 | 16 | # The ExpiredSignature class is raised when the JWT signature has expired. 17 | class ExpiredSignature < DecodeError; end 18 | 19 | # The IncorrectAlgorithm class is raised when the JWT algorithm is incorrect. 20 | class IncorrectAlgorithm < DecodeError; end 21 | 22 | # The ImmatureSignature class is raised when the JWT signature is immature. 23 | class ImmatureSignature < DecodeError; end 24 | 25 | # The InvalidIssuerError class is raised when the JWT issuer is invalid. 26 | class InvalidIssuerError < DecodeError; end 27 | 28 | # The UnsupportedEcdsaCurve class is raised when the ECDSA curve is unsupported. 29 | class UnsupportedEcdsaCurve < IncorrectAlgorithm; end 30 | 31 | # The InvalidIatError class is raised when the JWT issued at (iat) claim is invalid. 32 | class InvalidIatError < DecodeError; end 33 | 34 | # The InvalidAudError class is raised when the JWT audience (aud) claim is invalid. 35 | class InvalidAudError < DecodeError; end 36 | 37 | # The InvalidSubError class is raised when the JWT subject (sub) claim is invalid. 38 | class InvalidSubError < DecodeError; end 39 | 40 | # The InvalidCritError class is raised when the JWT crit header is invalid. 41 | class InvalidCritError < DecodeError; end 42 | 43 | # The InvalidJtiError class is raised when the JWT ID (jti) claim is invalid. 44 | class InvalidJtiError < DecodeError; end 45 | 46 | # The InvalidPayload class is raised when the JWT payload is invalid. 47 | class InvalidPayload < DecodeError; end 48 | 49 | # The MissingRequiredClaim class is raised when a required claim is missing from the JWT. 50 | class MissingRequiredClaim < DecodeError; end 51 | 52 | # The Base64DecodeError class is raised when there is an error decoding a Base64-encoded string. 53 | class Base64DecodeError < DecodeError; end 54 | 55 | # The JWKError class is raised when there is an error with the JSON Web Key (JWK). 56 | class JWKError < DecodeError; end 57 | end 58 | -------------------------------------------------------------------------------- /lib/jwt/json.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | 5 | module JWT 6 | # @api private 7 | class JSON 8 | class << self 9 | def generate(data) 10 | ::JSON.generate(data) 11 | end 12 | 13 | def parse(data) 14 | ::JSON.parse(data) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/jwt/jwa.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'openssl' 4 | 5 | require_relative 'jwa/signing_algorithm' 6 | require_relative 'jwa/ecdsa' 7 | require_relative 'jwa/hmac' 8 | require_relative 'jwa/none' 9 | require_relative 'jwa/ps' 10 | require_relative 'jwa/rsa' 11 | require_relative 'jwa/unsupported' 12 | 13 | module JWT 14 | # The JWA module contains all supported algorithms. 15 | module JWA 16 | class << self 17 | # @api private 18 | def resolve(algorithm) 19 | return find(algorithm) if algorithm.is_a?(String) || algorithm.is_a?(Symbol) 20 | 21 | raise ArgumentError, 'Custom algorithms are required to include JWT::JWA::SigningAlgorithm' unless algorithm.is_a?(SigningAlgorithm) 22 | 23 | algorithm 24 | end 25 | 26 | # @api private 27 | def resolve_and_sort(algorithms:, preferred_algorithm:) 28 | algs = Array(algorithms).map { |alg| JWA.resolve(alg) } 29 | algs.partition { |alg| alg.valid_alg?(preferred_algorithm) }.flatten 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/jwt/jwa/ecdsa.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module JWA 5 | # ECDSA signing algorithm 6 | class Ecdsa 7 | include JWT::JWA::SigningAlgorithm 8 | 9 | def initialize(alg, digest) 10 | @alg = alg 11 | @digest = OpenSSL::Digest.new(digest) 12 | end 13 | 14 | def sign(data:, signing_key:) 15 | curve_definition = curve_by_name(signing_key.group.curve_name) 16 | key_algorithm = curve_definition[:algorithm] 17 | raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} signing key was provided" if alg != key_algorithm 18 | 19 | asn1_to_raw(signing_key.dsa_sign_asn1(digest.digest(data)), signing_key) 20 | end 21 | 22 | def verify(data:, signature:, verification_key:) 23 | curve_definition = curve_by_name(verification_key.group.curve_name) 24 | key_algorithm = curve_definition[:algorithm] 25 | raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} verification key was provided" if alg != key_algorithm 26 | 27 | verification_key.dsa_verify_asn1(digest.digest(data), raw_to_asn1(signature, verification_key)) 28 | rescue OpenSSL::PKey::PKeyError 29 | raise JWT::VerificationError, 'Signature verification raised' 30 | end 31 | 32 | NAMED_CURVES = { 33 | 'prime256v1' => { 34 | algorithm: 'ES256', 35 | digest: 'sha256' 36 | }, 37 | 'secp256r1' => { # alias for prime256v1 38 | algorithm: 'ES256', 39 | digest: 'sha256' 40 | }, 41 | 'secp384r1' => { 42 | algorithm: 'ES384', 43 | digest: 'sha384' 44 | }, 45 | 'secp521r1' => { 46 | algorithm: 'ES512', 47 | digest: 'sha512' 48 | }, 49 | 'secp256k1' => { 50 | algorithm: 'ES256K', 51 | digest: 'sha256' 52 | } 53 | }.freeze 54 | 55 | NAMED_CURVES.each_value do |v| 56 | register_algorithm(new(v[:algorithm], v[:digest])) 57 | end 58 | 59 | def self.curve_by_name(name) 60 | NAMED_CURVES.fetch(name) do 61 | raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported" 62 | end 63 | end 64 | 65 | private 66 | 67 | attr_reader :digest 68 | 69 | def curve_by_name(name) 70 | self.class.curve_by_name(name) 71 | end 72 | 73 | def raw_to_asn1(signature, private_key) 74 | byte_size = (private_key.group.degree + 7) / 8 75 | sig_bytes = signature[0..(byte_size - 1)] 76 | sig_char = signature[byte_size..-1] || '' 77 | OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der 78 | end 79 | 80 | def asn1_to_raw(signature, public_key) 81 | byte_size = (public_key.group.degree + 7) / 8 82 | OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/jwt/jwa/hmac.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module JWA 5 | # Implementation of the HMAC family of algorithms 6 | class Hmac 7 | include JWT::JWA::SigningAlgorithm 8 | 9 | def initialize(alg, digest) 10 | @alg = alg 11 | @digest = digest 12 | end 13 | 14 | def sign(data:, signing_key:) 15 | signing_key ||= '' 16 | raise_verify_error!('HMAC key expected to be a String') unless signing_key.is_a?(String) 17 | 18 | OpenSSL::HMAC.digest(digest.new, signing_key, data) 19 | rescue OpenSSL::HMACError => e 20 | 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' 21 | 22 | raise e 23 | end 24 | 25 | def verify(data:, signature:, verification_key:) 26 | SecurityUtils.secure_compare(signature, sign(data: data, signing_key: verification_key)) 27 | end 28 | 29 | register_algorithm(new('HS256', OpenSSL::Digest::SHA256)) 30 | register_algorithm(new('HS384', OpenSSL::Digest::SHA384)) 31 | register_algorithm(new('HS512', OpenSSL::Digest::SHA512)) 32 | 33 | private 34 | 35 | attr_reader :digest 36 | 37 | # Copy of https://github.com/rails/rails/blob/v7.0.3.1/activesupport/lib/active_support/security_utils.rb 38 | # rubocop:disable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate 39 | module SecurityUtils 40 | # Constant time string comparison, for fixed length strings. 41 | # 42 | # The values compared should be of fixed length, such as strings 43 | # that have already been processed by HMAC. Raises in case of length mismatch. 44 | 45 | if defined?(OpenSSL.fixed_length_secure_compare) 46 | def fixed_length_secure_compare(a, b) 47 | OpenSSL.fixed_length_secure_compare(a, b) 48 | end 49 | else 50 | # :nocov: 51 | def fixed_length_secure_compare(a, b) 52 | raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize 53 | 54 | l = a.unpack "C#{a.bytesize}" 55 | 56 | res = 0 57 | b.each_byte { |byte| res |= byte ^ l.shift } 58 | res == 0 59 | end 60 | # :nocov: 61 | end 62 | module_function :fixed_length_secure_compare 63 | 64 | # Secure string comparison for strings of variable length. 65 | # 66 | # While a timing attack would not be able to discern the content of 67 | # a secret compared via secure_compare, it is possible to determine 68 | # the secret length. This should be considered when using secure_compare 69 | # to compare weak, short secrets to user input. 70 | def secure_compare(a, b) 71 | a.bytesize == b.bytesize && fixed_length_secure_compare(a, b) 72 | end 73 | module_function :secure_compare 74 | end 75 | # rubocop:enable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/jwt/jwa/none.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module JWA 5 | # Implementation of the none algorithm 6 | class None 7 | include JWT::JWA::SigningAlgorithm 8 | 9 | def initialize 10 | @alg = 'none' 11 | end 12 | 13 | def sign(*) 14 | '' 15 | end 16 | 17 | def verify(*) 18 | true 19 | end 20 | 21 | register_algorithm(new) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/jwt/jwa/ps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module JWA 5 | # Implementation of the RSASSA-PSS family of algorithms 6 | class Ps 7 | include JWT::JWA::SigningAlgorithm 8 | 9 | def initialize(alg) 10 | @alg = alg 11 | @digest_algorithm = alg.sub('PS', 'sha') 12 | end 13 | 14 | def sign(data:, signing_key:) 15 | 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) 16 | raise_sign_error!('The key length must be greater than or equal to 2048 bits') if signing_key.n.num_bits < 2048 17 | 18 | signing_key.sign_pss(digest_algorithm, data, salt_length: :digest, mgf1_hash: digest_algorithm) 19 | end 20 | 21 | def verify(data:, signature:, verification_key:) 22 | verification_key.verify_pss(digest_algorithm, signature, data, salt_length: :auto, mgf1_hash: digest_algorithm) 23 | rescue OpenSSL::PKey::PKeyError 24 | raise JWT::VerificationError, 'Signature verification raised' 25 | end 26 | 27 | register_algorithm(new('PS256')) 28 | register_algorithm(new('PS384')) 29 | register_algorithm(new('PS512')) 30 | 31 | private 32 | 33 | attr_reader :digest_algorithm 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/jwt/jwa/rsa.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module JWA 5 | # Implementation of the RSA family of algorithms 6 | class Rsa 7 | include JWT::JWA::SigningAlgorithm 8 | 9 | def initialize(alg) 10 | @alg = alg 11 | @digest = OpenSSL::Digest.new(alg.sub('RS', 'SHA')) 12 | end 13 | 14 | def sign(data:, signing_key:) 15 | 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) 16 | raise_sign_error!('The key length must be greater than or equal to 2048 bits') if signing_key.n.num_bits < 2048 17 | 18 | signing_key.sign(digest, data) 19 | end 20 | 21 | def verify(data:, signature:, verification_key:) 22 | verification_key.verify(digest, signature, data) 23 | rescue OpenSSL::PKey::PKeyError 24 | raise JWT::VerificationError, 'Signature verification raised' 25 | end 26 | 27 | register_algorithm(new('RS256')) 28 | register_algorithm(new('RS384')) 29 | register_algorithm(new('RS512')) 30 | 31 | private 32 | 33 | attr_reader :digest 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/jwt/jwa/signing_algorithm.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | # JSON Web Algorithms 5 | module JWA 6 | # Base functionality for signing algorithms 7 | module SigningAlgorithm 8 | # Class methods for the SigningAlgorithm module 9 | module ClassMethods 10 | def register_algorithm(algo) 11 | ::JWT::JWA.register_algorithm(algo) 12 | end 13 | end 14 | 15 | def self.included(klass) 16 | klass.extend(ClassMethods) 17 | end 18 | 19 | attr_reader :alg 20 | 21 | def valid_alg?(alg_to_check) 22 | alg&.casecmp(alg_to_check)&.zero? == true 23 | end 24 | 25 | def header(*) 26 | { 'alg' => alg } 27 | end 28 | 29 | def sign(*) 30 | raise_sign_error!('Algorithm implementation is missing the sign method') 31 | end 32 | 33 | def verify(*) 34 | raise_verify_error!('Algorithm implementation is missing the verify method') 35 | end 36 | 37 | def raise_verify_error!(message) 38 | raise(DecodeError.new(message).tap { |e| e.set_backtrace(caller(1)) }) 39 | end 40 | 41 | def raise_sign_error!(message) 42 | raise(EncodeError.new(message).tap { |e| e.set_backtrace(caller(1)) }) 43 | end 44 | end 45 | 46 | class << self 47 | def register_algorithm(algo) 48 | algorithms[algo.alg.to_s.downcase] = algo 49 | end 50 | 51 | def find(algo) 52 | algorithms.fetch(algo.to_s.downcase, Unsupported) 53 | end 54 | 55 | private 56 | 57 | def algorithms 58 | @algorithms ||= {} 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/jwt/jwa/unsupported.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module JWA 5 | # Represents an unsupported algorithm 6 | module Unsupported 7 | class << self 8 | include JWT::JWA::SigningAlgorithm 9 | 10 | def sign(*) 11 | raise_sign_error!('Unsupported signing method') 12 | end 13 | 14 | def verify(*) 15 | raise JWT::VerificationError, 'Algorithm not supported' 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/jwt/jwk.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'jwk/key_finder' 4 | require_relative 'jwk/set' 5 | 6 | module JWT 7 | # JSON Web Key (JWK) 8 | module JWK 9 | class << self 10 | def create_from(key, params = nil, options = {}) 11 | if key.is_a?(Hash) 12 | jwk_kty = key[:kty] || key['kty'] 13 | raise JWT::JWKError, 'Key type (kty) not provided' unless jwk_kty 14 | 15 | return mappings.fetch(jwk_kty.to_s) do |kty| 16 | raise JWT::JWKError, "Key type #{kty} not supported" 17 | end.new(key, params, options) 18 | end 19 | 20 | mappings.fetch(key.class) do |klass| 21 | raise JWT::JWKError, "Cannot create JWK from a #{klass.name}" 22 | end.new(key, params, options) 23 | end 24 | 25 | def classes 26 | @mappings = nil # reset the cached mappings 27 | @classes ||= [] 28 | end 29 | 30 | alias new create_from 31 | alias import create_from 32 | 33 | private 34 | 35 | def mappings 36 | @mappings ||= generate_mappings 37 | end 38 | 39 | def generate_mappings 40 | classes.each_with_object({}) do |klass, hash| 41 | next unless klass.const_defined?('KTYS') 42 | 43 | Array(klass::KTYS).each do |kty| 44 | hash[kty] = klass 45 | end 46 | end 47 | end 48 | end 49 | end 50 | end 51 | 52 | require_relative 'jwk/key_base' 53 | require_relative 'jwk/ec' 54 | require_relative 'jwk/rsa' 55 | require_relative 'jwk/hmac' 56 | -------------------------------------------------------------------------------- /lib/jwt/jwk/hmac.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module JWK 5 | # JWK for HMAC keys 6 | class HMAC < KeyBase 7 | KTY = 'oct' 8 | KTYS = [KTY, String, JWT::JWK::HMAC].freeze 9 | HMAC_PUBLIC_KEY_ELEMENTS = %i[kty].freeze 10 | HMAC_PRIVATE_KEY_ELEMENTS = %i[k].freeze 11 | HMAC_KEY_ELEMENTS = (HMAC_PRIVATE_KEY_ELEMENTS + HMAC_PUBLIC_KEY_ELEMENTS).freeze 12 | 13 | def initialize(key, params = nil, options = {}) 14 | params ||= {} 15 | 16 | # For backwards compatibility when kid was a String 17 | params = { kid: params } if params.is_a?(String) 18 | 19 | key_params = extract_key_params(key) 20 | 21 | params = params.transform_keys(&:to_sym) 22 | check_jwk(key_params, params) 23 | 24 | super(options, key_params.merge(params)) 25 | end 26 | 27 | def keypair 28 | secret 29 | end 30 | 31 | def private? 32 | true 33 | end 34 | 35 | def public_key 36 | nil 37 | end 38 | 39 | def verify_key 40 | secret 41 | end 42 | 43 | def signing_key 44 | secret 45 | end 46 | 47 | # See https://tools.ietf.org/html/rfc7517#appendix-A.3 48 | def export(options = {}) 49 | exported = parameters.clone 50 | exported.reject! { |k, _| HMAC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true 51 | exported 52 | end 53 | 54 | def members 55 | HMAC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] } 56 | end 57 | 58 | def key_digest 59 | sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key), 60 | OpenSSL::ASN1::UTF8String.new(KTY)]) 61 | OpenSSL::Digest::SHA256.hexdigest(sequence.to_der) 62 | end 63 | 64 | def []=(key, value) 65 | raise ArgumentError, 'cannot overwrite cryptographic key attributes' if HMAC_KEY_ELEMENTS.include?(key.to_sym) 66 | 67 | super 68 | end 69 | 70 | private 71 | 72 | def secret 73 | @secret ||= ::JWT::Base64.url_decode(self[:k]) 74 | end 75 | 76 | def extract_key_params(key) 77 | case key 78 | when JWT::JWK::HMAC 79 | key.export(include_private: true) 80 | when String # Accept String key as input 81 | { kty: KTY, k: ::JWT::Base64.url_encode(key) } 82 | when Hash 83 | key.transform_keys(&:to_sym) 84 | else 85 | raise ArgumentError, 'key must be of type String or Hash with key parameters' 86 | end 87 | end 88 | 89 | def check_jwk(keypair, params) 90 | raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (HMAC_KEY_ELEMENTS & params.keys).empty? 91 | raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY 92 | raise JWT::JWKError, 'Key format is invalid for HMAC' unless keypair[:k] 93 | end 94 | 95 | class << self 96 | def import(jwk_data) 97 | new(jwk_data) 98 | end 99 | end 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/jwt/jwk/key_base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module JWK 5 | # Base for JWK implementations 6 | class KeyBase 7 | def self.inherited(klass) 8 | super 9 | ::JWT::JWK.classes << klass 10 | end 11 | 12 | def initialize(options, params = {}) 13 | options ||= {} 14 | 15 | @parameters = params.transform_keys(&:to_sym) # Uniform interface 16 | 17 | # For backwards compatibility, kid_generator may be specified in the parameters 18 | options[:kid_generator] ||= @parameters.delete(:kid_generator) 19 | 20 | # Make sure the key has a kid 21 | kid_generator = options[:kid_generator] || ::JWT.configuration.jwk.kid_generator 22 | self[:kid] ||= kid_generator.new(self).generate 23 | end 24 | 25 | def kid 26 | self[:kid] 27 | end 28 | 29 | def hash 30 | self[:kid].hash 31 | end 32 | 33 | def [](key) 34 | @parameters[key.to_sym] 35 | end 36 | 37 | def []=(key, value) 38 | @parameters[key.to_sym] = value 39 | end 40 | 41 | def ==(other) 42 | other.is_a?(::JWT::JWK::KeyBase) && self[:kid] == other[:kid] 43 | end 44 | 45 | alias eql? == 46 | 47 | def <=>(other) 48 | return nil unless other.is_a?(::JWT::JWK::KeyBase) 49 | 50 | self[:kid] <=> other[:kid] 51 | end 52 | 53 | private 54 | 55 | attr_reader :parameters 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/jwt/jwk/key_finder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module JWK 5 | # JSON Web Key keyfinder 6 | # To find the key for a given kid 7 | class KeyFinder 8 | # Initializes a new KeyFinder instance. 9 | # @param [Hash] options the options to create a KeyFinder with 10 | # @option options [Proc, JWT::JWK::Set] :jwks the jwks or a loader proc 11 | # @option options [Boolean] :allow_nil_kid whether to allow nil kid 12 | def initialize(options) 13 | @allow_nil_kid = options[:allow_nil_kid] 14 | jwks_or_loader = options[:jwks] 15 | 16 | @jwks_loader = if jwks_or_loader.respond_to?(:call) 17 | jwks_or_loader 18 | else 19 | ->(_options) { jwks_or_loader } 20 | end 21 | end 22 | 23 | # Returns the verification key for the given kid 24 | # @param [String] kid the key id 25 | def key_for(kid) 26 | raise ::JWT::DecodeError, 'No key id (kid) found from token headers' unless kid || @allow_nil_kid 27 | raise ::JWT::DecodeError, 'Invalid type for kid header parameter' unless kid.nil? || kid.is_a?(String) 28 | 29 | jwk = resolve_key(kid) 30 | 31 | raise ::JWT::DecodeError, 'No keys found in jwks' unless @jwks.any? 32 | raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk 33 | 34 | jwk.verify_key 35 | end 36 | 37 | # Returns the key for the given token 38 | # @param [JWT::EncodedToken] token the token 39 | def call(token) 40 | key_for(token.header['kid']) 41 | end 42 | 43 | private 44 | 45 | def resolve_key(kid) 46 | key_matcher = ->(key) { (kid.nil? && @allow_nil_kid) || key[:kid] == kid } 47 | 48 | # First try without invalidation to facilitate application caching 49 | @jwks ||= JWT::JWK::Set.new(@jwks_loader.call(kid: kid)) 50 | jwk = @jwks.find { |key| key_matcher.call(key) } 51 | 52 | return jwk if jwk 53 | 54 | # Second try, invalidate for backwards compatibility 55 | @jwks = JWT::JWK::Set.new(@jwks_loader.call(invalidate: true, kid_not_found: true, kid: kid)) 56 | @jwks.find { |key| key_matcher.call(key) } 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/jwt/jwk/kid_as_key_digest.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module JWK 5 | # @api private 6 | class KidAsKeyDigest 7 | def initialize(jwk) 8 | @jwk = jwk 9 | end 10 | 11 | def generate 12 | @jwk.key_digest 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/jwt/jwk/rsa.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module JWK 5 | # JSON Web Key (JWK) representation of a RSA key 6 | class RSA < KeyBase # rubocop:disable Metrics/ClassLength 7 | BINARY = 2 8 | KTY = 'RSA' 9 | KTYS = [KTY, OpenSSL::PKey::RSA, JWT::JWK::RSA].freeze 10 | RSA_PUBLIC_KEY_ELEMENTS = %i[kty n e].freeze 11 | RSA_PRIVATE_KEY_ELEMENTS = %i[d p q dp dq qi].freeze 12 | RSA_KEY_ELEMENTS = (RSA_PRIVATE_KEY_ELEMENTS + RSA_PUBLIC_KEY_ELEMENTS).freeze 13 | 14 | RSA_OPT_PARAMS = %i[p q dp dq qi].freeze 15 | RSA_ASN1_SEQUENCE = (%i[n e d] + RSA_OPT_PARAMS).freeze # https://www.rfc-editor.org/rfc/rfc3447#appendix-A.1.2 16 | 17 | def initialize(key, params = nil, options = {}) 18 | params ||= {} 19 | 20 | # For backwards compatibility when kid was a String 21 | params = { kid: params } if params.is_a?(String) 22 | 23 | key_params = extract_key_params(key) 24 | 25 | params = params.transform_keys(&:to_sym) 26 | check_jwk_params!(key_params, params) 27 | 28 | super(options, key_params.merge(params)) 29 | end 30 | 31 | def keypair 32 | rsa_key 33 | end 34 | 35 | def private? 36 | rsa_key.private? 37 | end 38 | 39 | def public_key 40 | rsa_key.public_key 41 | end 42 | 43 | def signing_key 44 | rsa_key if private? 45 | end 46 | 47 | def verify_key 48 | rsa_key.public_key 49 | end 50 | 51 | def export(options = {}) 52 | exported = parameters.clone 53 | exported.reject! { |k, _| RSA_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true 54 | exported 55 | end 56 | 57 | def members 58 | RSA_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] } 59 | end 60 | 61 | def key_digest 62 | sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(public_key.n), 63 | OpenSSL::ASN1::Integer.new(public_key.e)]) 64 | OpenSSL::Digest::SHA256.hexdigest(sequence.to_der) 65 | end 66 | 67 | def []=(key, value) 68 | raise ArgumentError, 'cannot overwrite cryptographic key attributes' if RSA_KEY_ELEMENTS.include?(key.to_sym) 69 | 70 | super 71 | end 72 | 73 | private 74 | 75 | def rsa_key 76 | @rsa_key ||= self.class.create_rsa_key(jwk_attributes(*(RSA_KEY_ELEMENTS - [:kty]))) 77 | end 78 | 79 | def extract_key_params(key) 80 | case key 81 | when JWT::JWK::RSA 82 | key.export(include_private: true) 83 | when OpenSSL::PKey::RSA # Accept OpenSSL key as input 84 | @rsa_key = key # Preserve the object to avoid recreation 85 | parse_rsa_key(key) 86 | when Hash 87 | key.transform_keys(&:to_sym) 88 | else 89 | raise ArgumentError, 'key must be of type OpenSSL::PKey::RSA or Hash with key parameters' 90 | end 91 | end 92 | 93 | def check_jwk_params!(key_params, params) 94 | raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (RSA_KEY_ELEMENTS & params.keys).empty? 95 | raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY 96 | raise JWT::JWKError, 'Key format is invalid for RSA' unless key_params[:n] && key_params[:e] 97 | end 98 | 99 | def parse_rsa_key(key) 100 | { 101 | kty: KTY, 102 | n: encode_open_ssl_bn(key.n), 103 | e: encode_open_ssl_bn(key.e), 104 | d: encode_open_ssl_bn(key.d), 105 | p: encode_open_ssl_bn(key.p), 106 | q: encode_open_ssl_bn(key.q), 107 | dp: encode_open_ssl_bn(key.dmp1), 108 | dq: encode_open_ssl_bn(key.dmq1), 109 | qi: encode_open_ssl_bn(key.iqmp) 110 | }.compact 111 | end 112 | 113 | def jwk_attributes(*attributes) 114 | attributes.each_with_object({}) do |attribute, hash| 115 | hash[attribute] = decode_open_ssl_bn(self[attribute]) 116 | end 117 | end 118 | 119 | def encode_open_ssl_bn(key_part) 120 | return unless key_part 121 | 122 | ::JWT::Base64.url_encode(key_part.to_s(BINARY)) 123 | end 124 | 125 | def decode_open_ssl_bn(jwk_data) 126 | self.class.decode_open_ssl_bn(jwk_data) 127 | end 128 | 129 | class << self 130 | def import(jwk_data) 131 | new(jwk_data) 132 | end 133 | 134 | def decode_open_ssl_bn(jwk_data) 135 | return nil unless jwk_data 136 | 137 | OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY) 138 | end 139 | 140 | def create_rsa_key_using_der(rsa_parameters) 141 | validate_rsa_parameters!(rsa_parameters) 142 | 143 | sequence = RSA_ASN1_SEQUENCE.each_with_object([]) do |key, arr| 144 | next if rsa_parameters[key].nil? 145 | 146 | arr << OpenSSL::ASN1::Integer.new(rsa_parameters[key]) 147 | end 148 | 149 | if sequence.size > 2 # Append "two-prime" version for private key 150 | sequence.unshift(OpenSSL::ASN1::Integer.new(0)) 151 | 152 | 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 153 | end 154 | 155 | OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der) 156 | end 157 | 158 | def create_rsa_key_using_sets(rsa_parameters) 159 | validate_rsa_parameters!(rsa_parameters) 160 | 161 | OpenSSL::PKey::RSA.new.tap do |rsa_key| 162 | rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d]) 163 | rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q] 164 | rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi] 165 | end 166 | end 167 | 168 | # :nocov: 169 | # Before openssl 2.0, we need to use the accessors to set the key 170 | def create_rsa_key_using_accessors(rsa_parameters) # rubocop:disable Metrics/AbcSize 171 | validate_rsa_parameters!(rsa_parameters) 172 | 173 | OpenSSL::PKey::RSA.new.tap do |rsa_key| 174 | rsa_key.n = rsa_parameters[:n] 175 | rsa_key.e = rsa_parameters[:e] 176 | rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d] 177 | rsa_key.p = rsa_parameters[:p] if rsa_parameters[:p] 178 | rsa_key.q = rsa_parameters[:q] if rsa_parameters[:q] 179 | rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp] 180 | rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq] 181 | rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi] 182 | end 183 | end 184 | # :nocov: 185 | 186 | def validate_rsa_parameters!(rsa_parameters) 187 | return unless rsa_parameters.key?(:d) 188 | 189 | parameters = RSA_OPT_PARAMS - rsa_parameters.keys 190 | return if parameters.empty? || parameters.size == RSA_OPT_PARAMS.size 191 | 192 | 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 193 | end 194 | 195 | if ::JWT.openssl_3? 196 | alias create_rsa_key create_rsa_key_using_der 197 | elsif OpenSSL::PKey::RSA.new.respond_to?(:set_key) 198 | alias create_rsa_key create_rsa_key_using_sets 199 | else 200 | alias create_rsa_key create_rsa_key_using_accessors 201 | end 202 | end 203 | end 204 | end 205 | end 206 | -------------------------------------------------------------------------------- /lib/jwt/jwk/set.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'forwardable' 4 | 5 | module JWT 6 | module JWK 7 | # JSON Web Key Set (JWKS) representation 8 | # https://tools.ietf.org/html/rfc7517 9 | class Set 10 | include Enumerable 11 | extend Forwardable 12 | 13 | attr_reader :keys 14 | 15 | def initialize(jwks = nil, options = {}) # rubocop:disable Metrics/CyclomaticComplexity 16 | jwks ||= {} 17 | 18 | @keys = case jwks 19 | when JWT::JWK::Set # Simple duplication 20 | jwks.keys 21 | when JWT::JWK::KeyBase # Singleton 22 | [jwks] 23 | when Hash 24 | jwks = jwks.transform_keys(&:to_sym) 25 | [*jwks[:keys]].map { |k| JWT::JWK.new(k, nil, options) } 26 | when Array 27 | jwks.map { |k| JWT::JWK.new(k, nil, options) } 28 | else 29 | raise ArgumentError, 'Can only create new JWKS from Hash, Array and JWK' 30 | end 31 | end 32 | 33 | def export(options = {}) 34 | { keys: @keys.map { |k| k.export(options) } } 35 | end 36 | 37 | def_delegators :@keys, :each, :size, :delete, :dig 38 | 39 | def select!(&block) 40 | return @keys.select! unless block 41 | 42 | self if @keys.select!(&block) 43 | end 44 | 45 | def reject!(&block) 46 | return @keys.reject! unless block 47 | 48 | self if @keys.reject!(&block) 49 | end 50 | 51 | def uniq!(&block) 52 | self if @keys.uniq!(&block) 53 | end 54 | 55 | def merge(enum) 56 | @keys += JWT::JWK::Set.new(enum.to_a).keys 57 | self 58 | end 59 | 60 | def union(enum) 61 | dup.merge(enum) 62 | end 63 | 64 | def add(key) 65 | @keys << JWT::JWK.new(key) 66 | self 67 | end 68 | 69 | def ==(other) 70 | other.is_a?(JWT::JWK::Set) && keys.sort == other.keys.sort 71 | end 72 | 73 | alias eql? == 74 | alias filter! select! 75 | alias length size 76 | # For symbolic manipulation 77 | alias | union 78 | alias + union 79 | alias << add 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/jwt/jwk/thumbprint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | module JWK 5 | # https://tools.ietf.org/html/rfc7638 6 | class Thumbprint 7 | attr_reader :jwk 8 | 9 | def initialize(jwk) 10 | @jwk = jwk 11 | end 12 | 13 | def generate 14 | ::Base64.urlsafe_encode64( 15 | Digest::SHA256.digest( 16 | JWT::JSON.generate( 17 | jwk.members.sort.to_h 18 | ) 19 | ), padding: false 20 | ) 21 | end 22 | 23 | alias to_s generate 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/jwt/token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | # Represents a JWT token 5 | # 6 | # Basic token signed using the HS256 algorithm: 7 | # 8 | # token = JWT::Token.new(payload: {pay: 'load'}) 9 | # token.sign!(algorithm: 'HS256', key: 'secret') 10 | # token.jwt # => eyJhb.... 11 | # 12 | # Custom headers will be combined with generated headers: 13 | # token = JWT::Token.new(payload: {pay: 'load'}, header: {custom: "value"}) 14 | # token.sign!(algorithm: 'HS256', key: 'secret') 15 | # token.header # => {"custom"=>"value", "alg"=>"HS256"} 16 | # 17 | class Token 18 | # Initializes a new Token instance. 19 | # 20 | # @param header [Hash] the header of the JWT token. 21 | # @param payload [Hash] the payload of the JWT token. 22 | def initialize(payload:, header: {}) 23 | @header = header&.transform_keys(&:to_s) 24 | @payload = payload 25 | end 26 | 27 | # Returns the decoded signature of the JWT token. 28 | # 29 | # @return [String] the decoded signature of the JWT token. 30 | def signature 31 | @signature ||= ::JWT::Base64.url_decode(encoded_signature || '') 32 | end 33 | 34 | # Returns the encoded signature of the JWT token. 35 | # 36 | # @return [String] the encoded signature of the JWT token. 37 | def encoded_signature 38 | @encoded_signature ||= ::JWT::Base64.url_encode(signature) 39 | end 40 | 41 | # Returns the decoded header of the JWT token. 42 | # 43 | # @return [Hash] the header of the JWT token. 44 | attr_reader :header 45 | 46 | # Returns the encoded header of the JWT token. 47 | # 48 | # @return [String] the encoded header of the JWT token. 49 | def encoded_header 50 | @encoded_header ||= ::JWT::Base64.url_encode(JWT::JSON.generate(header)) 51 | end 52 | 53 | # Returns the payload of the JWT token. 54 | # 55 | # @return [Hash] the payload of the JWT token. 56 | attr_reader :payload 57 | 58 | # Returns the encoded payload of the JWT token. 59 | # 60 | # @return [String] the encoded payload of the JWT token. 61 | def encoded_payload 62 | @encoded_payload ||= ::JWT::Base64.url_encode(JWT::JSON.generate(payload)) 63 | end 64 | 65 | # Returns the signing input of the JWT token. 66 | # 67 | # @return [String] the signing input of the JWT token. 68 | def signing_input 69 | @signing_input ||= [encoded_header, encoded_payload].join('.') 70 | end 71 | 72 | # Returns the JWT token as a string. 73 | # 74 | # @return [String] the JWT token as a string. 75 | # @raise [JWT::EncodeError] if the token is not signed or other encoding issues 76 | def jwt 77 | @jwt ||= (@signature && [encoded_header, @detached_payload ? '' : encoded_payload, encoded_signature].join('.')) || raise(::JWT::EncodeError, 'Token is not signed') 78 | end 79 | 80 | # Detaches the payload according to https://datatracker.ietf.org/doc/html/rfc7515#appendix-F 81 | # 82 | def detach_payload! 83 | @detached_payload = true 84 | 85 | nil 86 | end 87 | 88 | # Signs the JWT token. 89 | # 90 | # @param algorithm [String, Object] the algorithm to use for signing. 91 | # @param key [String] the key to use for signing. 92 | # @return [void] 93 | # @raise [JWT::EncodeError] if the token is already signed or other problems when signing 94 | def sign!(algorithm:, key:) 95 | raise ::JWT::EncodeError, 'Token already signed' if @signature 96 | 97 | JWA.resolve(algorithm).tap do |algo| 98 | header.merge!(algo.header) { |_key, old, _new| old } 99 | @signature = algo.sign(data: signing_input, signing_key: key) 100 | end 101 | 102 | nil 103 | end 104 | 105 | # Verifies the claims of the token. 106 | # @param options [Array, Hash] the claims to verify. 107 | # @raise [JWT::DecodeError] if the claims are invalid. 108 | def verify_claims!(*options) 109 | Claims::Verifier.verify!(self, *options) 110 | end 111 | 112 | # Returns the errors of the claims of the token. 113 | # @param options [Array, Hash] the claims to verify. 114 | # @return [Array] the errors of the claims. 115 | def claim_errors(*options) 116 | Claims::Verifier.errors(self, *options) 117 | end 118 | 119 | # Returns whether the claims of the token are valid. 120 | # @param options [Array, Hash] the claims to verify. 121 | # @return [Boolean] whether the claims are valid. 122 | def valid_claims?(*options) 123 | claim_errors(*options).empty? 124 | end 125 | 126 | # Returns the JWT token as a string. 127 | # 128 | # @return [String] the JWT token as a string. 129 | alias to_s jwt 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/jwt/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # JSON Web Token implementation 4 | # 5 | # Should be up to date with the latest spec: 6 | # https://tools.ietf.org/html/rfc7519 7 | module JWT 8 | # Returns the gem version of the JWT library. 9 | # 10 | # @return [Gem::Version] the gem version. 11 | def self.gem_version 12 | Gem::Version.new(VERSION::STRING) 13 | end 14 | 15 | # Version constants 16 | module VERSION 17 | MAJOR = 3 18 | MINOR = 0 19 | TINY = 0 20 | PRE = 'beta1' 21 | 22 | STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') 23 | end 24 | 25 | # Checks if the OpenSSL version is 3 or greater. 26 | # 27 | # @return [Boolean] true if OpenSSL version is 3 or greater, false otherwise. 28 | # @api private 29 | def self.openssl_3? 30 | return false if OpenSSL::OPENSSL_VERSION.include?('LibreSSL') 31 | 32 | true if 3 * 0x10000000 <= OpenSSL::OPENSSL_VERSION_NUMBER 33 | end 34 | 35 | # Checks if there is an OpenSSL 3 HMAC empty key regression. 36 | # 37 | # @return [Boolean] true if there is an OpenSSL 3 HMAC empty key regression, false otherwise. 38 | # @api private 39 | def self.openssl_3_hmac_empty_key_regression? 40 | openssl_3? && openssl_version <= ::Gem::Version.new('3.0.0') 41 | end 42 | 43 | # Returns the OpenSSL version. 44 | # 45 | # @return [Gem::Version] the OpenSSL version. 46 | # @api private 47 | def self.openssl_version 48 | @openssl_version ||= ::Gem::Version.new(OpenSSL::VERSION) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/jwt/x5c_key_finder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JWT 4 | # If the x5c header certificate chain can be validated by trusted root 5 | # certificates, and none of the certificates are revoked, returns the public 6 | # key from the first certificate. 7 | # See https://tools.ietf.org/html/rfc7515#section-4.1.6 8 | class X5cKeyFinder 9 | def initialize(root_certificates, crls = nil) 10 | raise ArgumentError, 'Root certificates must be specified' unless root_certificates 11 | 12 | @store = build_store(root_certificates, crls) 13 | end 14 | 15 | def from(x5c_header_or_certificates) 16 | signing_certificate, *certificate_chain = parse_certificates(x5c_header_or_certificates) 17 | store_context = OpenSSL::X509::StoreContext.new(@store, signing_certificate, certificate_chain) 18 | 19 | if store_context.verify 20 | signing_certificate.public_key 21 | else 22 | error = "Certificate verification failed: #{store_context.error_string}." 23 | if (current_cert = store_context.current_cert) 24 | error = "#{error} Certificate subject: #{current_cert.subject}." 25 | end 26 | 27 | raise JWT::VerificationError, error 28 | end 29 | end 30 | 31 | private 32 | 33 | def build_store(root_certificates, crls) 34 | store = OpenSSL::X509::Store.new 35 | store.purpose = OpenSSL::X509::PURPOSE_ANY 36 | store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK | OpenSSL::X509::V_FLAG_CRL_CHECK_ALL 37 | root_certificates.each { |certificate| store.add_cert(certificate) } 38 | crls&.each { |crl| store.add_crl(crl) } 39 | store 40 | end 41 | 42 | def parse_certificates(x5c_header_or_certificates) 43 | if x5c_header_or_certificates.all? { |obj| obj.is_a?(OpenSSL::X509::Certificate) } 44 | x5c_header_or_certificates 45 | else 46 | x5c_header_or_certificates.map do |encoded| 47 | OpenSSL::X509::Certificate.new(::JWT::Base64.url_decode(encoded)) 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /ruby-jwt.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'jwt/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'jwt' 9 | spec.version = JWT.gem_version 10 | spec.authors = [ 11 | 'Tim Rudat' 12 | ] 13 | spec.email = 'timrudat@gmail.com' 14 | spec.summary = 'JSON Web Token implementation in Ruby' 15 | spec.description = 'A pure ruby implementation of the RFC 7519 OAuth JSON Web Token (JWT) standard.' 16 | spec.homepage = 'https://github.com/jwt/ruby-jwt' 17 | spec.license = 'MIT' 18 | spec.required_ruby_version = '>= 2.5' 19 | spec.metadata = { 20 | 'bug_tracker_uri' => 'https://github.com/jwt/ruby-jwt/issues', 21 | 'changelog_uri' => "https://github.com/jwt/ruby-jwt/blob/v#{JWT.gem_version}/CHANGELOG.md", 22 | 'rubygems_mfa_required' => 'true' 23 | } 24 | 25 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 26 | f.match(%r{^(spec|gemfiles|coverage|bin)/}) || # Irrelevant folders 27 | f.match(/^\.+/) || # Files and folders starting with . 28 | f.match(/^(Appraisals|Gemfile|Rakefile)$/) # Irrelevant files 29 | end 30 | 31 | spec.executables = [] 32 | spec.require_paths = %w[lib] 33 | 34 | spec.add_dependency 'base64' 35 | 36 | spec.add_development_dependency 'appraisal' 37 | spec.add_development_dependency 'bundler' 38 | spec.add_development_dependency 'logger' 39 | spec.add_development_dependency 'rake' 40 | spec.add_development_dependency 'rspec' 41 | spec.add_development_dependency 'rubocop' 42 | spec.add_development_dependency 'simplecov' 43 | end 44 | -------------------------------------------------------------------------------- /spec/fixtures/keys/ec256-private-v2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIFZpgytOAXPVreqGsHPdD9pojw30bnlqfUAqFZ3V3/qeoAoGCCqGSM49 3 | AwEHoUQDQgAE7JbAf3pWEEPje6NG+4dGOwIZnNwRFIe7DnQ4xFWKPrL5tVWlBh7N 4 | DFhjGNhiyO+aQjbcx9uWV74ifq7i21Bemg== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /spec/fixtures/keys/ec256-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIJmVse5uPfj6B4TcXrUAvf9/8pJh+KrKKYLNcmOnp/vPoAoGCCqGSM49 3 | AwEHoUQDQgAEAr+WbDE5VtIDGhtYMxvEc6cMsDBc/DX1wuhIMu8dQzOLSt0tpqK9 4 | MVfXbVfrKdayVFgoWzs8MilcYq0QIhKx/w== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /spec/fixtures/keys/ec256-public-v2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7JbAf3pWEEPje6NG+4dGOwIZnNwR 3 | FIe7DnQ4xFWKPrL5tVWlBh7NDFhjGNhiyO+aQjbcx9uWV74ifq7i21Bemg== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /spec/fixtures/keys/ec256-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAr+WbDE5VtIDGhtYMxvEc6cMsDBc 3 | /DX1wuhIMu8dQzOLSt0tpqK9MVfXbVfrKdayVFgoWzs8MilcYq0QIhKx/w== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /spec/fixtures/keys/ec256-wrong-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEPmuXZT3jpJnEMVPOW6RMsmxeGLOCE1PN 3 | 6fwvUwOsxv7YnyoQ5/bpo64n+Jp4slSl1aUNoCBF2oz9bS0iyBo3jg== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /spec/fixtures/keys/ec256k-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHQCAQEEIMTine3s8tT+8bswDM4/z8o+wIYGb9PQPrw8x6Nu6QDdoAcGBSuBBAAK 3 | oUQDQgAEy8wuv6+fXodLPLfhxm132y1R8m4dkng7tHe7N+sULV2Eth6AxEXQfd+E 4 | 4nuceR21UNCvQKqxiYwCzVwIKcHe/A== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /spec/fixtures/keys/ec256k-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEy8wuv6+fXodLPLfhxm132y1R8m4dkng7 3 | tHe7N+sULV2Eth6AxEXQfd+E4nuceR21UNCvQKqxiYwCzVwIKcHe/A== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /spec/fixtures/keys/ec384-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MIGkAgEBBDDxOljqUKw9YNhkluSJIBAYO1YXcNtS+vckd5hpTZ5toxsOlwbmyrnU 3 | Tn+D5Xma1m2gBwYFK4EEACKhZANiAASQwYTiRvXu1hMHceSosMs/8uf50sJI3jvK 4 | kdSkvuRAPxSzhtrUvCQDnVsThFq4aOdZZY1qh2ErJGtzmrx+pEsJvJnvfOTG3NGU 5 | KRalek+LQfVqAUSvDMKlxdkz2e67tso= 6 | -----END EC PRIVATE KEY----- 7 | -------------------------------------------------------------------------------- /spec/fixtures/keys/ec384-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEkMGE4kb17tYTB3HkqLDLP/Ln+dLCSN47 3 | ypHUpL7kQD8Us4ba1LwkA51bE4RauGjnWWWNaodhKyRrc5q8fqRLCbyZ73zkxtzR 4 | lCkWpXpPi0H1agFErwzCpcXZM9nuu7bK 5 | -----END PUBLIC KEY----- 6 | -------------------------------------------------------------------------------- /spec/fixtures/keys/ec512-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MIHcAgEBBEIB0/+ffxEj7j62xvGaB5pvzk888e412ESO/EK/K0QlS9dSF8+Rj1rG 3 | zqpRB8fvDnoe8xdmkW/W5GKzojMyv7YQYumgBwYFK4EEACOhgYkDgYYABAEw74Yw 4 | aTbPY6TtWmxx6LJDzCX2nKWCPnKdZcEH9Ncu8g5RjRBRq2yacja3OoS6nA2YeDng 5 | reBJxZr376P6Ns6XcQFWDA6K/MCTrEBCsPxXZNxd8KR9vMGWhgNtWRrcKzwJfQkr 6 | suyehZkbbYyFnAWyARKHZuV7VUXmeEmRS/f93MPqVA== 7 | -----END EC PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /spec/fixtures/keys/ec512-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBMO+GMGk2z2Ok7VpsceiyQ8wl9pyl 3 | gj5ynWXBB/TXLvIOUY0QUatsmnI2tzqEupwNmHg54K3gScWa9++j+jbOl3EBVgwO 4 | ivzAk6xAQrD8V2TcXfCkfbzBloYDbVka3Cs8CX0JK7LsnoWZG22MhZwFsgESh2bl 5 | e1VF5nhJkUv3/dzD6lQ= 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /spec/fixtures/keys/rsa-2048-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA4GzZTLU48c4WbyvHi+QKrB71x+T0eq5hqDbQqnlYjhD1Ika7 3 | io1iplsdJWJuyxfYbUkb2Ol0fj4koZ/GS6lgCZr4+8UHbr1qf0Eu5HZSpszs2YxY 4 | 8U5RHnrpw67co7hlgAR9HbyNf5XIYgLV9ldHH/eazwnc3F/hgNsV0xjScVilejgo 5 | cJ4zcsyymvW8t42lteM7bI867ZuJhGop/V+Y0HFyrMsPoQyLuCUpr6ulOfrkr7ZO 6 | dhAIG8r1HcjOp/AUjM15vfXcbUZjkM/VloifX1YitU3upMGJ8/DpFGffMOImrn5r 7 | 6BT494V8rRyN2qvQoAkLJpqZ0avLxwiR2lgVQQIDAQABAoIBAEH0Ozgr2fxWEInD 8 | V/VooypKPvjr9F1JejGxSkmPN9MocKIOH3dsbZ1uEXa3ItBUxan4XlK06SNgp+tH 9 | xULfF/Y6sQlsse59hBq50Uoa69dRShn1AP6JgZVvkduMPBNxUYL5zrs6emsQXb9Q 10 | DglDRQfEAJ7vyxSIqQDxYcyT8uSUF70dqFe+E9B2VE3D6ccHc98k41pJrAFAUFH1 11 | wwvDhfyYr7/Ultut9wzpZvU1meF3Vna3GOUHfxrG6wu1G+WIWHGjouzThsc1qiVI 12 | BtMCJxuCt5fOXRbU4STbMqhB6sZHiOh6J/dZU6JwRYt+IS8FB6kCNFSEWZWQledJ 13 | XqtYSQECgYEA9nmnFTRj3fTBq9zMXfCRujkSy6X2bOb39ftNXzHFuc+I6xmv/3Bs 14 | P9tDdjueP/SnCb7i/9hXkpEIcxjrjiqgcvD2ym1hE4q+odMzRAXYMdnmzI34SVZE 15 | U5hYJcYsXNKrTTleba7QgqdORmyJ9FwqLO40udvmrZMY223XDwgRkOkCgYEA6RkO 16 | 5wjjrWWp/G1YN3KXZTS1m2/eGrUThohXKAfAjbWWiouNLW2msXrxEWsPRL6xKiHu 17 | X9cwZwzi3MstAgk+bphUGUVUkGKNDjWHJA25tDYjbPtkd6xbL4eCHsKpNL3HNYr9 18 | N0CIvgn7qjaHRBem0iK7T6keY4axaSVddEwYapkCgYEA13K5qaB1F4Smcpt8DTWH 19 | vPe8xUUaZlFzOJLmLCsuwmB2N8Ppg2j7RspcaxJsH021YaB5ftjWm+ipMSr8ZPY/ 20 | 8JlPsNzxuYpTXtNmAbT2KYVm6THEch61dTk6/DIBf1YrpUJbl5by7vJeStL/uBmE 21 | SGgksL5XIyzs0opuLdaIvFkCgYAyBLWE8AxjFfCvAQuwAj/ocLITo6KmWnrRIIqL 22 | RXaVMgUWv7FQsTnW1cnK8g05tC2yG8vZ9wQk6Mf5lwOWb0NdWgSZ0528ydj41pWk 23 | L+nMeN2LMjqxz2NVxJ8wWJcUgTCxFZ0WcRumo9/D+6V1ABpE9zz4cBLcSnfhVypB 24 | nV6T6QKBgQCSZNCQ9HPxjAgYcsqc5sjNwuN1GHQZSav3Tye3k6zHENe1lsteT9K8 25 | xciGIuhybKZBvB4yImIIHCtnH+AS+mHAGqHarjNDMfvjOq0dMibPx4+bkIiHdBIH 26 | Xz+j5kmntvFiUnzr0Z/Tcqo+r8FvyCo1YWgwqGP8XoFrswD7gy7cZw== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /spec/fixtures/keys/rsa-2048-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4GzZTLU48c4WbyvHi+QK 3 | rB71x+T0eq5hqDbQqnlYjhD1Ika7io1iplsdJWJuyxfYbUkb2Ol0fj4koZ/GS6lg 4 | CZr4+8UHbr1qf0Eu5HZSpszs2YxY8U5RHnrpw67co7hlgAR9HbyNf5XIYgLV9ldH 5 | H/eazwnc3F/hgNsV0xjScVilejgocJ4zcsyymvW8t42lteM7bI867ZuJhGop/V+Y 6 | 0HFyrMsPoQyLuCUpr6ulOfrkr7ZOdhAIG8r1HcjOp/AUjM15vfXcbUZjkM/Vloif 7 | X1YitU3upMGJ8/DpFGffMOImrn5r6BT494V8rRyN2qvQoAkLJpqZ0avLxwiR2lgV 8 | QQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /spec/fixtures/keys/rsa-2048-wrong-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzHAVGaW9j4l3/b4ngcjj 3 | oIoIcnsQEWOMqErb5VhLZMGIq1gEO5qxPDAwooKsNotzcAOB3ZyLn7p5D+dmOrNU 4 | YkYWgYITNGeSifrnVqQugd5Fh1L8K7zOGltUo2UtjbN4uJ56tzxBMZp2wejs2/Qu 5 | 0eu0xZK3To+YkDcWOk92rmNgmUSQC/kNyIOj+yBvOo3wTk6HvbhoIarCgJ6Lay1v 6 | /hMLyQLzwRY/Qfty1FTIDyTv2dch47FsfkZ1KAL+MbUnHuCBPzGxRjXa8Iy9Z7YG 7 | xrYasUt1b0um64bscxoIiCu8yLL8jlg01Rwrjr/MTwKRhwXlMp8B7HTonwtaG6ar 8 | JwIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /spec/jwt/claims/audience_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::Claims::Audience do 4 | let(:payload) { { 'nbf' => (Time.now.to_i + 5) } } 5 | 6 | describe '#verify!' do 7 | let(:scalar_aud) { 'ruby-jwt-aud' } 8 | let(:array_aud) { %w[ruby-jwt-aud test-aud ruby-ruby-ruby] } 9 | 10 | subject(:verify!) { described_class.new(expected_audience: expected_audience).verify!(context: SpecSupport::Token.new(payload: payload)) } 11 | 12 | context 'when the singular audience does not match' do 13 | let(:expected_audience) { 'no-match' } 14 | let(:payload) { { 'aud' => scalar_aud } } 15 | 16 | it 'raises JWT::InvalidAudError' do 17 | expect do 18 | subject 19 | end.to raise_error JWT::InvalidAudError 20 | end 21 | end 22 | 23 | context 'when the payload has an array and none match the supplied value' do 24 | let(:expected_audience) { 'no-match' } 25 | let(:payload) { { 'aud' => array_aud } } 26 | 27 | it 'raises JWT::InvalidAudError' do 28 | expect do 29 | subject 30 | end.to raise_error JWT::InvalidAudError 31 | end 32 | end 33 | 34 | context 'when single audience is required' do 35 | let(:expected_audience) { scalar_aud } 36 | let(:payload) { { 'aud' => scalar_aud } } 37 | 38 | it 'passes validation' do 39 | subject 40 | end 41 | end 42 | 43 | context 'when any value in payload matches a single expected' do 44 | let(:expected_audience) { array_aud.first } 45 | let(:payload) { { 'aud' => array_aud } } 46 | 47 | it 'passes validation' do 48 | subject 49 | end 50 | end 51 | 52 | context 'when an array with any value matching the one in the options' do 53 | let(:expected_audience) { array_aud.first } 54 | let(:payload) { { 'aud' => array_aud } } 55 | 56 | it 'passes validation' do 57 | subject 58 | end 59 | end 60 | 61 | context 'when an array with any value matching all in the options' do 62 | let(:expected_audience) { array_aud } 63 | let(:payload) { { 'aud' => array_aud } } 64 | 65 | it 'passes validation' do 66 | subject 67 | end 68 | end 69 | 70 | context 'when a singular audience payload matching any value in the options array' do 71 | let(:expected_audience) { array_aud } 72 | let(:payload) { { 'aud' => scalar_aud } } 73 | 74 | it 'passes validation' do 75 | subject 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/jwt/claims/crit_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::Claims::Crit do 4 | subject(:verify!) { described_class.new(expected_crits: expected_crits).verify!(context: SpecSupport::Token.new(header: header)) } 5 | let(:expected_crits) { [] } 6 | let(:header) { {} } 7 | 8 | context 'when header is missing' do 9 | it 'raises JWT::InvalidCritError' do 10 | expect { verify! }.to raise_error(JWT::InvalidCritError, 'Crit header missing') 11 | end 12 | end 13 | 14 | context 'when header is not an array' do 15 | let(:header) { { 'crit' => 'not_an_array' } } 16 | 17 | it 'raises JWT::InvalidCritError' do 18 | expect { verify! }.to raise_error(JWT::InvalidCritError, 'Crit header should be an array') 19 | end 20 | end 21 | 22 | context 'when header is an array and not containing the expected value' do 23 | let(:header) { { 'crit' => %w[crit1] } } 24 | let(:expected_crits) { %w[crit2] } 25 | it 'raises an InvalidCritError' do 26 | expect { verify! }.to raise_error(JWT::InvalidCritError, 'Crit header missing expected values: crit2') 27 | end 28 | end 29 | 30 | context 'when header is an array containing exactly the expected values' do 31 | let(:header) { { 'crit' => %w[crit1 crit2] } } 32 | let(:expected_crits) { %w[crit1 crit2] } 33 | it 'does not raise an error' do 34 | expect(verify!).to eq(nil) 35 | end 36 | end 37 | 38 | context 'when header is an array containing at least the expected values' do 39 | let(:header) { { 'crit' => %w[crit1 crit2 crit3] } } 40 | let(:expected_crits) { %w[crit1 crit2] } 41 | it 'does not raise an error' do 42 | expect(verify!).to eq(nil) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/jwt/claims/expiration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::Claims::Expiration do 4 | let(:payload) { { 'exp' => (Time.now.to_i + 5) } } 5 | let(:leeway) { 0 } 6 | 7 | subject(:verify!) { described_class.new(leeway: leeway).verify!(context: SpecSupport::Token.new(payload: payload)) } 8 | 9 | context 'when token is expired' do 10 | let(:payload) { { 'exp' => (Time.now.to_i - 5) } } 11 | 12 | it 'must raise JWT::ExpiredSignature when the token has expired' do 13 | expect { verify! }.to(raise_error(JWT::ExpiredSignature)) 14 | end 15 | end 16 | 17 | context 'when token is expired but some leeway is defined' do 18 | let(:payload) { { 'exp' => (Time.now.to_i - 5) } } 19 | let(:leeway) { 10 } 20 | 21 | it 'passes validation' do 22 | verify! 23 | end 24 | end 25 | 26 | context 'when token exp is set to current time' do 27 | let(:payload) { { 'exp' => Time.now.to_i } } 28 | 29 | it 'fails validation' do 30 | expect { verify! }.to(raise_error(JWT::ExpiredSignature)) 31 | end 32 | end 33 | 34 | context 'when token is not a Hash' do 35 | let(:payload) { 'beautyexperts_nbf_iat' } 36 | it 'passes validation' do 37 | verify! 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/jwt/claims/issued_at_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::Claims::IssuedAt do 4 | let(:payload) { { 'iat' => Time.now.to_f } } 5 | 6 | subject(:verify!) { described_class.new.verify!(context: SpecSupport::Token.new(payload: payload)) } 7 | 8 | context 'when iat is now' do 9 | it 'passes validation' do 10 | verify! 11 | end 12 | end 13 | 14 | context 'when iat is now as a integer' do 15 | let(:payload) { { 'iat' => Time.now.to_i } } 16 | 17 | it 'passes validation' do 18 | verify! 19 | end 20 | end 21 | context 'when iat is not a number' do 22 | let(:payload) { { 'iat' => 'not_a_number' } } 23 | 24 | it 'fails validation' do 25 | expect { verify! }.to raise_error(JWT::InvalidIatError) 26 | end 27 | end 28 | 29 | context 'when iat is in the future' do 30 | let(:payload) { { 'iat' => Time.now.to_f + 120.0 } } 31 | 32 | it 'fails validation' do 33 | expect { verify! }.to raise_error(JWT::InvalidIatError) 34 | end 35 | end 36 | 37 | context 'when payload is a string containing iat' do 38 | let(:payload) { 'beautyexperts_nbf_iat' } 39 | 40 | it 'passes validation' do 41 | verify! 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/jwt/claims/issuer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::Claims::Issuer do 4 | let(:issuer) { 'ruby-jwt-gem' } 5 | let(:payload) { { 'iss' => issuer } } 6 | let(:expected_issuers) { 'ruby-jwt-gem' } 7 | 8 | subject(:verify!) { described_class.new(issuers: expected_issuers).verify!(context: SpecSupport::Token.new(payload: payload)) } 9 | 10 | context 'when expected issuer is a string that matches the payload' do 11 | it 'passes validation' do 12 | verify! 13 | end 14 | end 15 | 16 | context 'when expected issuer is a string that does not match the payload' do 17 | let(:issuer) { 'mismatched-issuer' } 18 | it 'raises JWT::InvalidIssuerError' do 19 | expect { verify! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected ["ruby-jwt-gem"], received mismatched-issuer') 20 | end 21 | end 22 | 23 | context 'when payload does not contain any issuer' do 24 | let(:payload) { {} } 25 | it 'raises JWT::InvalidIssuerError' do 26 | expect { verify! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected ["ruby-jwt-gem"], received ') 27 | end 28 | end 29 | 30 | context 'when expected issuer is an array that matches the payload' do 31 | let(:expected_issuers) { ['first', issuer, 'third'] } 32 | it 'passes validation' do 33 | verify! 34 | end 35 | end 36 | 37 | context 'when expected issuer is an array that does not match the payload' do 38 | let(:expected_issuers) { %w[first second] } 39 | it 'raises JWT::InvalidIssuerError' do 40 | expect { verify! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected ["first", "second"], received ruby-jwt-gem') 41 | end 42 | end 43 | 44 | context 'when expected issuer is an array and payload does not have any issuer' do 45 | let(:payload) { {} } 46 | let(:expected_issuers) { %w[first second] } 47 | it 'raises JWT::InvalidIssuerError' do 48 | expect { verify! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected ["first", "second"], received ') 49 | end 50 | end 51 | 52 | context 'when issuer is given as a RegExp' do 53 | let(:issuer) { 'ruby-jwt-gem' } 54 | let(:expected_issuers) { /\A(first|#{issuer}|third)\z/ } 55 | it 'passes validation' do 56 | verify! 57 | end 58 | end 59 | 60 | context 'when issuer is given as a RegExp and does not match the payload' do 61 | let(:issuer) { 'mismatched-issuer' } 62 | let(:expected_issuers) { /\A(first|second)\z/ } 63 | it 'raises JWT::InvalidIssuerError' do 64 | expect { verify! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected [/\A(first|second)\z/], received mismatched-issuer') 65 | end 66 | end 67 | 68 | context 'when issuer is given as a RegExp and payload does not have any issuer' do 69 | let(:payload) { {} } 70 | let(:expected_issuers) { /\A(first|second)\z/ } 71 | it 'raises JWT::InvalidIssuerError' do 72 | expect { verify! }.to raise_error(JWT::InvalidIssuerError, 'Invalid issuer. Expected [/\A(first|second)\z/], received ') 73 | end 74 | end 75 | 76 | context 'when issuer is given as a Proc' do 77 | let(:issuer) { 'ruby-jwt-gem' } 78 | let(:expected_issuers) { ->(iss) { iss.start_with?('ruby') } } 79 | it 'passes validation' do 80 | verify! 81 | end 82 | end 83 | 84 | context 'when issuer is given as a Proc and does not match the payload' do 85 | let(:issuer) { 'mismatched-issuer' } 86 | let(:expected_issuers) { ->(iss) { iss.start_with?('ruby') } } 87 | it 'raises JWT::InvalidIssuerError' do 88 | expect { verify! }.to raise_error(JWT::InvalidIssuerError, /received mismatched-issuer/) 89 | end 90 | end 91 | 92 | context 'when issuer is given as a Proc and payload does not have any issuer' do 93 | let(:payload) { {} } 94 | let(:expected_issuers) { ->(iss) { iss&.start_with?('ruby') } } 95 | it 'raises JWT::InvalidIssuerError' do 96 | expect { verify! }.to raise_error(JWT::InvalidIssuerError, /received /) 97 | end 98 | end 99 | 100 | context 'when issuer is given as a Method instance' do 101 | def issuer_start_with_ruby?(issuer) 102 | issuer&.start_with?('ruby') 103 | end 104 | 105 | let(:issuer) { 'ruby-jwt-gem' } 106 | let(:expected_issuers) { method(:issuer_start_with_ruby?) } 107 | 108 | it 'passes validation' do 109 | verify! 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/jwt/claims/jwt_id_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::Claims::JwtId do 4 | let(:jti) { 'some-random-uuid-or-whatever' } 5 | let(:payload) { { 'jti' => jti } } 6 | let(:validator) { nil } 7 | 8 | subject(:verify!) { described_class.new(validator: validator).verify!(context: SpecSupport::Token.new(payload: payload)) } 9 | context 'when payload contains a jti' do 10 | it 'passes validation' do 11 | verify! 12 | end 13 | end 14 | 15 | context 'when payload is missing a jti' do 16 | let(:payload) { {} } 17 | it 'raises JWT::InvalidJtiError' do 18 | expect { verify! }.to raise_error(JWT::InvalidJtiError, 'Missing jti') 19 | end 20 | end 21 | 22 | context 'when payload contains a jti that is an empty string' do 23 | let(:jti) { '' } 24 | it 'raises JWT::InvalidJtiError' do 25 | expect { verify! }.to raise_error(JWT::InvalidJtiError, 'Missing jti') 26 | end 27 | end 28 | 29 | context 'when payload contains a jti that is a blank string' do 30 | let(:jti) { ' ' } 31 | it 'raises JWT::InvalidJtiError' do 32 | expect { verify! }.to raise_error(JWT::InvalidJtiError, 'Missing jti') 33 | end 34 | end 35 | 36 | context 'when jti validator is a proc returning false' do 37 | let(:validator) { ->(_jti) { false } } 38 | it 'raises JWT::InvalidJtiError' do 39 | expect { verify! }.to raise_error(JWT::InvalidJtiError, 'Invalid jti') 40 | end 41 | end 42 | 43 | context 'when jti validator is a proc returning true' do 44 | let(:validator) { ->(_jti) { true } } 45 | it 'passes validation' do 46 | verify! 47 | end 48 | end 49 | 50 | context 'when jti validator has 2 args' do 51 | let(:validator) { ->(_jti, _pl) { true } } 52 | it 'passes validation' do 53 | verify! 54 | end 55 | end 56 | 57 | context 'when jti validator has 2 args' do 58 | it 'the second arg is the payload' do 59 | described_class.new(validator: ->(_jti, pl) { expect(pl).to eq(payload) }).verify!(context: SpecSupport::Token.new(payload: payload)) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/jwt/claims/not_before_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::Claims::NotBefore do 4 | let(:payload) { { 'nbf' => (Time.now.to_i + 5) } } 5 | 6 | describe '#verify!' do 7 | context 'when nbf is in the future' do 8 | it 'raises JWT::ImmatureSignature' do 9 | expect { described_class.new(leeway: 0).verify!(context: SpecSupport::Token.new(payload: payload)) }.to raise_error JWT::ImmatureSignature 10 | end 11 | end 12 | 13 | context 'when nbf is in the past' do 14 | let(:payload) { { 'nbf' => (Time.now.to_i - 5) } } 15 | 16 | it 'does not raise error' do 17 | expect { described_class.new(leeway: 0).verify!(context: SpecSupport::Token.new(payload: payload)) }.not_to raise_error 18 | end 19 | end 20 | 21 | context 'when leeway is given' do 22 | it 'does not raise error' do 23 | expect { described_class.new(leeway: 10).verify!(context: SpecSupport::Token.new(payload: payload)) }.not_to raise_error 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/jwt/claims/numeric_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::Claims::Numeric do 4 | shared_examples_for 'a NumericDate claim' do |claim| 5 | context "when #{claim} payload is an integer" do 6 | let(:claims) { { claim => 12_345 } } 7 | 8 | it 'does not raise error' do 9 | expect { subject }.not_to raise_error 10 | end 11 | 12 | context 'and key is a string' do 13 | let(:claims) { { claim.to_s => 43.32 } } 14 | 15 | it 'does not raise error' do 16 | expect { subject }.not_to raise_error 17 | end 18 | end 19 | end 20 | 21 | context "when #{claim} payload is a float" do 22 | let(:claims) { { claim => 43.32 } } 23 | 24 | it 'does not raise error' do 25 | expect { subject }.not_to raise_error 26 | end 27 | end 28 | 29 | context "when #{claim} payload is a string" do 30 | let(:claims) { { claim => '1' } } 31 | 32 | it 'raises error' do 33 | expect { subject }.to raise_error JWT::InvalidPayload 34 | end 35 | 36 | context 'and key is a string' do 37 | let(:claims) { { claim.to_s => '1' } } 38 | 39 | it 'raises error' do 40 | expect { subject }.to raise_error JWT::InvalidPayload 41 | end 42 | end 43 | end 44 | 45 | context "when #{claim} payload is a Time object" do 46 | let(:claims) { { claim => Time.now } } 47 | 48 | it 'raises error' do 49 | expect { subject }.to raise_error JWT::InvalidPayload 50 | end 51 | end 52 | 53 | context "when #{claim} payload is a string" do 54 | let(:claims) { { claim => '1' } } 55 | 56 | it 'raises error' do 57 | expect { subject }.to raise_error JWT::InvalidPayload 58 | end 59 | end 60 | end 61 | 62 | let(:validator) { described_class.new } 63 | 64 | describe '#verify!' do 65 | subject { validator.verify!(context: JWT::Claims::VerificationContext.new(payload: claims)) } 66 | context 'exp claim' do 67 | it_should_behave_like 'a NumericDate claim', :exp 68 | end 69 | 70 | context 'iat claim' do 71 | it_should_behave_like 'a NumericDate claim', :iat 72 | end 73 | 74 | context 'nbf claim' do 75 | it_should_behave_like 'a NumericDate claim', :nbf 76 | end 77 | end 78 | 79 | describe 'use via ::JWT::Claims.verify_payload!' do 80 | subject { JWT::Claims.verify_payload!(claims, :numeric) } 81 | 82 | context 'exp claim' do 83 | it_should_behave_like 'a NumericDate claim', :exp 84 | end 85 | 86 | context 'iat claim' do 87 | it_should_behave_like 'a NumericDate claim', :iat 88 | end 89 | 90 | context 'nbf claim' do 91 | it_should_behave_like 'a NumericDate claim', :nbf 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/jwt/claims/required_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::Claims::Required do 4 | let(:payload) { { 'data' => 'value' } } 5 | 6 | subject(:verify!) { described_class.new(required_claims: required_claims).verify!(context: SpecSupport::Token.new(payload: payload)) } 7 | 8 | context 'when payload is missing the required claim' do 9 | let(:required_claims) { ['exp'] } 10 | it 'raises JWT::MissingRequiredClaim' do 11 | expect { verify! }.to raise_error JWT::MissingRequiredClaim, 'Missing required claim exp' 12 | end 13 | end 14 | 15 | context 'when payload has the required claims' do 16 | let(:payload) { { 'exp' => 'exp', 'custom_claim' => true } } 17 | let(:required_claims) { %w[exp custom_claim] } 18 | it 'passes validation' do 19 | verify! 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/jwt/claims/verifier_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::Claims::Verifier do 4 | describe '.verify!' do 5 | context 'when all claims are given' do 6 | let(:options) do 7 | [ 8 | :exp, 9 | :nbf, 10 | { iss: 'issuer' }, 11 | :iat, 12 | :jti, 13 | { aud: 'aud' }, 14 | :sub, 15 | :crit, 16 | { required: [] }, 17 | :numeric 18 | ] 19 | end 20 | 21 | it 'verifies all claims' do 22 | token = SpecSupport::Token.new(payload: { 'iss' => 'issuer', 'jti' => 1, 'aud' => 'aud' }, header: { 'crit' => [] }) 23 | expect(described_class.verify!(token, *options)).to eq(nil) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/jwt/claims_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::Claims do 4 | let(:payload) { { 'pay' => 'load' } } 5 | describe '.verify_payload!' do 6 | context 'when required_claims is passed' do 7 | it 'raises error' do 8 | expect { described_class.verify_payload!(payload, required: ['exp']) }.to raise_error(JWT::MissingRequiredClaim, 'Missing required claim exp') 9 | end 10 | end 11 | 12 | context 'exp claim' do 13 | let(:payload) { { 'exp' => Time.now.to_i - 10, 'pay' => 'load' } } 14 | 15 | it 'verifies the exp' do 16 | described_class.verify_payload!(payload, required: ['exp']) 17 | expect { described_class.verify_payload!(payload, exp: {}) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') 18 | described_class.verify_payload!(payload, exp: { leeway: 1000 }) 19 | end 20 | 21 | context 'when claims given as symbol' do 22 | it 'validates the claim' do 23 | expect { described_class.verify_payload!(payload, :exp) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') 24 | end 25 | end 26 | 27 | context 'when claims given as a list of symbols' do 28 | it 'validates the claim' do 29 | expect { described_class.verify_payload!(payload, :exp, :nbf) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') 30 | end 31 | end 32 | 33 | context 'when claims given as a list of symbols and hashes' do 34 | it 'validates the claim' do 35 | expect { described_class.verify_payload!(payload, { exp: { leeway: 1000 }, nbf: {} }, :exp, :nbf) }.to raise_error(JWT::ExpiredSignature, 'Signature has expired') 36 | end 37 | end 38 | end 39 | end 40 | 41 | describe '.valid_payload?' do 42 | context 'exp claim' do 43 | let(:payload) { { 'exp' => Time.now.to_i - 10, 'pay' => 'load' } } 44 | 45 | context 'when claim is valid' do 46 | it 'returns true' do 47 | expect(described_class.valid_payload?(payload, exp: { leeway: 1000 })).to be(true) 48 | end 49 | end 50 | 51 | context 'when claim is invalid' do 52 | it 'returns false' do 53 | expect(described_class.valid_payload?(payload, :exp)).to be(false) 54 | end 55 | end 56 | end 57 | 58 | context 'various types of params' do 59 | context 'when payload is missing most of the claims' do 60 | it 'raises an error' do 61 | expect do 62 | described_class.verify_payload!(payload, 63 | :nbf, 64 | iss: ['www.host.com', 'https://other.host.com'].freeze, 65 | aud: 'aud', 66 | exp: { leeway: 10 }) 67 | end.to raise_error(JWT::InvalidIssuerError) 68 | end 69 | end 70 | 71 | context 'when payload has everything that is expected of it' do 72 | let(:payload) { { 'iss' => 'www.host.com', 'aud' => 'audience', 'exp' => Time.now.to_i - 10, 'pay' => 'load' } } 73 | 74 | it 'does not raise' do 75 | expect do 76 | described_class.verify_payload!(payload, 77 | :nbf, 78 | iss: ['www.host.com', 'https://other.host.com'].freeze, 79 | aud: 'audience', 80 | exp: { leeway: 11 }) 81 | end.not_to raise_error 82 | end 83 | end 84 | end 85 | end 86 | 87 | describe '.payload_errors' do 88 | context 'exp claim' do 89 | let(:payload) { { 'exp' => Time.now.to_i - 10, 'pay' => 'load' } } 90 | 91 | context 'when claim is valid' do 92 | it 'returns empty array' do 93 | expect(described_class.payload_errors(payload, exp: { leeway: 1000 })).to be_empty 94 | end 95 | end 96 | 97 | context 'when claim is invalid' do 98 | it 'returns array with error objects' do 99 | expect(described_class.payload_errors(payload, :exp).map(&:message)).to eq(['Signature has expired']) 100 | end 101 | end 102 | end 103 | 104 | context 'various types of params' do 105 | let(:payload) { { 'exp' => Time.now.to_i - 10, 'pay' => 'load' } } 106 | 107 | context 'when payload is most of the claims' do 108 | it 'raises an error' do 109 | messages = described_class.payload_errors(payload, 110 | :nbf, 111 | iss: ['www.host.com', 'https://other.host.com'].freeze, 112 | aud: 'aud', 113 | exp: { leeway: 10 }).map(&:message) 114 | expect(messages).to eq(['Invalid issuer. Expected ["www.host.com", "https://other.host.com"], received ', 115 | 'Invalid audience. Expected aud, received ', 116 | 'Signature has expired']) 117 | end 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /spec/jwt/configuration/jwk_configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::Configuration::JwkConfiguration do 4 | describe '.kid_generator_type=' do 5 | context 'when invalid value is passed' do 6 | it 'raises ArgumentError' do 7 | expect { subject.kid_generator_type = :foo }.to raise_error(ArgumentError, 'foo is not a valid kid generator type.') 8 | end 9 | end 10 | 11 | context 'when valid value is passed' do 12 | it 'sets the generator matching the value' do 13 | subject.kid_generator_type = :rfc7638_thumbprint 14 | expect(subject.kid_generator).to eq(JWT::JWK::Thumbprint) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/jwt/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT do 4 | describe 'JWT.configure' do 5 | it 'yields the configuration' do 6 | expect { |b| described_class.configure(&b) }.to yield_with_args(described_class.configuration) 7 | end 8 | 9 | it 'allows configuration to be changed via the block' do 10 | expect(described_class.configuration.decode.verify_expiration).to eq(true) 11 | 12 | described_class.configure do |config| 13 | config.decode.verify_expiration = false 14 | end 15 | 16 | expect(described_class.configuration.decode.verify_expiration).to eq(false) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/jwt/jwa/ecdsa_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::JWA::Ecdsa do 4 | describe '.curve_by_name' do 5 | subject { described_class.curve_by_name(curve_name) } 6 | 7 | context 'when secp256r1 is given' do 8 | let(:curve_name) { 'secp256r1' } 9 | it { is_expected.to eq(algorithm: 'ES256', digest: 'sha256') } 10 | end 11 | 12 | context 'when prime256v1 is given' do 13 | let(:curve_name) { 'prime256v1' } 14 | it { is_expected.to eq(algorithm: 'ES256', digest: 'sha256') } 15 | end 16 | 17 | context 'when secp521r1 is given' do 18 | let(:curve_name) { 'secp521r1' } 19 | it { is_expected.to eq(algorithm: 'ES512', digest: 'sha512') } 20 | end 21 | 22 | context 'when secp256k1 is given' do 23 | let(:curve_name) { 'secp256k1' } 24 | it { is_expected.to eq(algorithm: 'ES256K', digest: 'sha256') } 25 | end 26 | 27 | context 'when unknown is given' do 28 | let(:curve_name) { 'unknown' } 29 | it 'raises an error' do 30 | expect { subject }.to raise_error(JWT::UnsupportedEcdsaCurve) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/jwt/jwa/hmac_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::JWA::Hmac do 4 | let(:instance) { described_class.new('HS256', OpenSSL::Digest::SHA256) } 5 | 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*') } 6 | let(:hmac_secret) { 'secret_key' } 7 | 8 | describe '#sign' do 9 | subject { instance.sign(data: 'test', signing_key: hmac_secret) } 10 | 11 | context 'when signing with a key' do 12 | it { is_expected.to eq(valid_signature) } 13 | end 14 | 15 | # Address OpenSSL 3.0 errors with empty hmac_secret - https://github.com/jwt/ruby-jwt/issues/526 16 | context 'when nil hmac_secret is passed' do 17 | let(:hmac_secret) { nil } 18 | context 'when OpenSSL 3.0 raises a malloc failure' do 19 | before do 20 | allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('EVP_PKEY_new_mac_key: malloc failure')) 21 | end 22 | 23 | it 'raises JWT::DecodeError' do 24 | expect { subject }.to raise_error(JWT::DecodeError, 'OpenSSL 3.0 does not support nil or empty hmac_secret') 25 | end 26 | end 27 | 28 | context 'when OpenSSL raises any other error' do 29 | before do 30 | allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('Another Random Error')) 31 | end 32 | 33 | it 'raises the original error' do 34 | expect { subject }.to raise_error(OpenSSL::HMACError, 'Another Random Error') 35 | end 36 | end 37 | 38 | context 'when other versions of openssl do not raise an exception' do 39 | let(:response) { Base64.decode64("Q7DO+ZJl+eNMEOqdNQGSbSezn1fG1nRWHYuiNueoGfs=\n") } 40 | before do 41 | allow(OpenSSL::HMAC).to receive(:digest).and_return(response) 42 | end 43 | 44 | it { is_expected.to eql(response) } 45 | end 46 | end 47 | 48 | context 'when blank hmac_secret is passed' do 49 | let(:hmac_secret) { '' } 50 | context 'when OpenSSL 3.0 raises a malloc failure' do 51 | before do 52 | allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('EVP_PKEY_new_mac_key: malloc failure')) 53 | end 54 | 55 | it 'raises JWT::DecodeError' do 56 | expect { subject }.to raise_error(JWT::DecodeError, 'OpenSSL 3.0 does not support nil or empty hmac_secret') 57 | end 58 | end 59 | 60 | context 'when OpenSSL raises any other error' do 61 | before do 62 | allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('Another Random Error')) 63 | end 64 | 65 | it 'raises the original error' do 66 | expect { subject }.to raise_error(OpenSSL::HMACError, 'Another Random Error') 67 | end 68 | end 69 | 70 | context 'when other versions of openssl do not raise an exception' do 71 | let(:response) { Base64.decode64("Q7DO+ZJl+eNMEOqdNQGSbSezn1fG1nRWHYuiNueoGfs=\n") } 72 | before do 73 | allow(OpenSSL::HMAC).to receive(:digest).and_return(response) 74 | end 75 | 76 | it { is_expected.to eql(response) } 77 | end 78 | end 79 | 80 | context 'when hmac_secret is passed' do 81 | let(:hmac_secret) { 'test' } 82 | context 'when OpenSSL 3.0 raises a malloc failure' do 83 | before do 84 | allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('EVP_PKEY_new_mac_key: malloc failure')) 85 | end 86 | 87 | it 'raises the original error' do 88 | expect { subject }.to raise_error(OpenSSL::HMACError, 'EVP_PKEY_new_mac_key: malloc failure') 89 | end 90 | end 91 | 92 | context 'when OpenSSL raises any other error' do 93 | before do 94 | allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('Another Random Error')) 95 | end 96 | 97 | it 'raises the original error' do 98 | expect { subject }.to raise_error(OpenSSL::HMACError, 'Another Random Error') 99 | end 100 | end 101 | 102 | context 'when other versions of openssl do not raise an exception' do 103 | let(:response) { Base64.decode64("iM0hCLU0fZc885zfkFPX3UJwSHbYyam9ji0WglnT3fc=\n") } 104 | before do 105 | allow(OpenSSL::HMAC).to receive(:digest).and_return(response) 106 | end 107 | 108 | it { is_expected.to eql(response) } 109 | end 110 | end 111 | end 112 | 113 | describe '#verify' do 114 | subject { instance.verify(data: 'test', signature: signature, verification_key: hmac_secret) } 115 | 116 | context 'when signature is valid' do 117 | let(:signature) { valid_signature } 118 | 119 | it { is_expected.to be(true) } 120 | end 121 | 122 | context 'when signature is invalid' do 123 | let(:signature) { [60, 56, 87, 72, 185, 194].pack('C*') } 124 | 125 | it { is_expected.to be(false) } 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /spec/jwt/jwa/none_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::JWA::None do 4 | subject { described_class.new } 5 | 6 | describe '#sign' do 7 | it 'returns an empty string' do 8 | expect(subject.sign('data', 'key')).to eq('') 9 | end 10 | end 11 | 12 | describe '#verify' do 13 | it 'returns true' do 14 | expect(subject.verify('data', 'signature', 'key')).to be true 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/jwt/jwa/ps_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::JWA::Ps do 4 | let(:rsa_key) { test_pkey('rsa-2048-private.pem') } 5 | let(:data) { 'test data' } 6 | let(:ps256_instance) { described_class.new('PS256') } 7 | let(:ps384_instance) { described_class.new('PS384') } 8 | let(:ps512_instance) { described_class.new('PS512') } 9 | 10 | describe '#initialize' do 11 | it 'initializes with the correct algorithm and digest' do 12 | expect(ps256_instance.instance_variable_get(:@alg)).to eq('PS256') 13 | expect(ps256_instance.send(:digest_algorithm)).to eq('sha256') 14 | 15 | expect(ps384_instance.instance_variable_get(:@alg)).to eq('PS384') 16 | expect(ps384_instance.send(:digest_algorithm)).to eq('sha384') 17 | 18 | expect(ps512_instance.instance_variable_get(:@alg)).to eq('PS512') 19 | expect(ps512_instance.send(:digest_algorithm)).to eq('sha512') 20 | end 21 | end 22 | 23 | describe '#sign' do 24 | context 'with a valid RSA key' do 25 | it 'signs the data with PS256' do 26 | expect(ps256_instance.sign(data: data, signing_key: rsa_key)).not_to be_nil 27 | end 28 | 29 | it 'signs the data with PS384' do 30 | expect(ps384_instance.sign(data: data, signing_key: rsa_key)).not_to be_nil 31 | end 32 | 33 | it 'signs the data with PS512' do 34 | expect(ps512_instance.sign(data: data, signing_key: rsa_key)).not_to be_nil 35 | end 36 | end 37 | 38 | context 'with an invalid key' do 39 | it 'raises an error' do 40 | expect do 41 | ps256_instance.sign(data: data, signing_key: 'invalid_key') 42 | end.to raise_error(JWT::EncodeError, /The given key is a String. It has to be an OpenSSL::PKey::RSA instance./) 43 | end 44 | end 45 | 46 | context 'with a key length less than 2048 bits' do 47 | let(:rsa_key) { OpenSSL::PKey::RSA.generate(2047) } 48 | 49 | it 'raises an error' do 50 | expect do 51 | ps256_instance.sign(data: data, signing_key: rsa_key) 52 | end.to raise_error(JWT::EncodeError, 'The key length must be greater than or equal to 2048 bits') 53 | end 54 | end 55 | end 56 | 57 | describe '#verify' do 58 | let(:ps256_signature) { ps256_instance.sign(data: data, signing_key: rsa_key) } 59 | let(:ps384_signature) { ps384_instance.sign(data: data, signing_key: rsa_key) } 60 | let(:ps512_signature) { ps512_instance.sign(data: data, signing_key: rsa_key) } 61 | 62 | context 'with a valid RSA key' do 63 | it 'verifies the signature with PS256' do 64 | expect(ps256_instance.verify(data: data, signature: ps256_signature, verification_key: rsa_key)).to be(true) 65 | end 66 | 67 | it 'verifies the signature with PS384' do 68 | expect(ps384_instance.verify(data: data, signature: ps384_signature, verification_key: rsa_key)).to be(true) 69 | end 70 | 71 | it 'verifies the signature with PS512' do 72 | expect(ps512_instance.verify(data: data, signature: ps512_signature, verification_key: rsa_key)).to be(true) 73 | end 74 | end 75 | 76 | context 'with an invalid signature' do 77 | it 'raises a verification error' do 78 | expect(ps256_instance.verify(data: data, signature: 'invalid_signature', verification_key: rsa_key)).to be(false) 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/jwt/jwa/rsa_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::JWA::Rsa do 4 | let(:rsa_key) { test_pkey('rsa-2048-private.pem') } 5 | let(:data) { 'test data' } 6 | let(:rsa_instance) { described_class.new('RS256') } 7 | 8 | describe '#initialize' do 9 | it 'initializes with the correct algorithm and digest' do 10 | expect(rsa_instance.instance_variable_get(:@alg)).to eq('RS256') 11 | expect(rsa_instance.send(:digest).name).to eq('SHA256') 12 | end 13 | end 14 | 15 | describe '#sign' do 16 | context 'with a valid RSA key' do 17 | it 'signs the data' do 18 | signature = rsa_instance.sign(data: data, signing_key: rsa_key) 19 | expect(signature).not_to be_nil 20 | end 21 | end 22 | 23 | context 'with a key length less than 2048 bits' do 24 | let(:rsa_key) { OpenSSL::PKey::RSA.generate(2047) } 25 | 26 | it 'raises an error' do 27 | expect do 28 | rsa_instance.sign(data: data, signing_key: rsa_key) 29 | end.to raise_error(JWT::EncodeError, 'The key length must be greater than or equal to 2048 bits') 30 | end 31 | end 32 | 33 | context 'with an invalid key' do 34 | it 'raises an error' do 35 | expect do 36 | rsa_instance.sign(data: data, signing_key: 'invalid_key') 37 | end.to raise_error(JWT::EncodeError, /The given key is a String. It has to be an OpenSSL::PKey::RSA instance/) 38 | end 39 | end 40 | end 41 | 42 | describe '#verify' do 43 | let(:signature) { rsa_instance.sign(data: data, signing_key: rsa_key) } 44 | 45 | context 'with a valid RSA key' do 46 | it 'returns true' do 47 | expect(rsa_instance.verify(data: data, signature: signature, verification_key: rsa_key)).to be(true) 48 | end 49 | end 50 | 51 | context 'with an invalid signature' do 52 | it 'returns false' do 53 | expect(rsa_instance.verify(data: data, signature: 'invalid_signature', verification_key: rsa_key)).to be(false) 54 | end 55 | end 56 | 57 | context 'with an invalid key' do 58 | it 'returns false' do 59 | expect(rsa_instance.verify(data: data, signature: 'invalid_signature', verification_key: OpenSSL::PKey::RSA.generate(2048))).to be(false) 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/jwt/jwa/unsupported_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::JWA::Unsupported do 4 | describe '.sign' do 5 | it 'raises an error for unsupported signing method' do 6 | expect { described_class.sign('data', 'key') }.to raise_error(JWT::EncodeError, 'Unsupported signing method') 7 | end 8 | end 9 | 10 | describe '.verify' do 11 | it 'raises an error for unsupported algorithm' do 12 | expect { described_class.verify('data', 'signature', 'key') }.to raise_error(JWT::VerificationError, 'Algorithm not supported') 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/jwt/jwa_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::JWA do 4 | describe '.resolve_and_sort' do 5 | let(:subject) { described_class.resolve_and_sort(algorithms: algorithms, preferred_algorithm: preferred_algorithm).map(&:alg) } 6 | 7 | context 'when algorithms have the preferred last' do 8 | let(:algorithms) { %w[HS256 HS512 RS512] } 9 | let(:preferred_algorithm) { 'RS512' } 10 | 11 | it 'places the preferred algorithm first' do 12 | is_expected.to eq(%w[RS512 HS256 HS512]) 13 | end 14 | end 15 | 16 | context 'when algorithms have the preferred in the middle' do 17 | let(:algorithms) { %w[HS512 HS256 RS512] } 18 | let(:preferred_algorithm) { 'HS256' } 19 | 20 | it 'places the preferred algorithm first' do 21 | is_expected.to eq(%w[HS256 HS512 RS512]) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/jwt/jwk/decode_with_jwk_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT do 4 | describe '.decode for JWK usecase' do 5 | let(:keypair) { test_pkey('rsa-2048-private.pem') } 6 | let(:jwk) { JWT::JWK.new(keypair) } 7 | let(:public_jwks) { { keys: [jwk.export, { kid: 'not_the_correct_one', kty: 'oct', k: 'secret' }] } } 8 | let(:token_payload) { { 'data' => 'something' } } 9 | let(:token_headers) { { kid: jwk.kid } } 10 | let(:algorithm) { 'RS512' } 11 | let(:signed_token) { described_class.encode(token_payload, jwk.signing_key, algorithm, token_headers) } 12 | 13 | context 'when JWK features are used manually' do 14 | it 'is able to decode the token' do 15 | payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm] }) do |header, _payload| 16 | JWT::JWK.import(public_jwks[:keys].find { |key| key[:kid] == header['kid'] }).verify_key 17 | end 18 | expect(payload).to eq(token_payload) 19 | end 20 | end 21 | 22 | context 'when jwk keys are given as an array' do 23 | context 'and kid is in the set' do 24 | it 'is able to decode the token' do 25 | payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) 26 | expect(payload).to eq(token_payload) 27 | end 28 | end 29 | 30 | context 'and kid is not in the set' do 31 | before do 32 | public_jwks[:keys].first[:kid] = 'NOT_A_MATCH' 33 | end 34 | it 'raises an exception' do 35 | expect { described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) }.to raise_error( 36 | JWT::DecodeError, /Could not find public key for kid .*/ 37 | ) 38 | end 39 | end 40 | 41 | context 'no keys are found in the set' do 42 | let(:public_jwks) { { keys: [] } } 43 | it 'raises an exception' do 44 | expect { described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) }.to raise_error( 45 | JWT::DecodeError, /No keys found in jwks/ 46 | ) 47 | end 48 | end 49 | 50 | context 'token does not know the kid' do 51 | let(:token_headers) { {} } 52 | it 'raises an exception' do 53 | expect { described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) }.to raise_error( 54 | JWT::DecodeError, 'No key id (kid) found from token headers' 55 | ) 56 | end 57 | end 58 | end 59 | 60 | context 'when jwk keys are loaded using a proc/lambda' do 61 | it 'decodes the token' do 62 | payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: ->(_opts) { public_jwks } }) 63 | expect(payload).to eq(token_payload) 64 | end 65 | end 66 | 67 | context 'when jwk keys are rotated' do 68 | it 'decodes the token' do 69 | key_loader = ->(options) { options[:invalidate] ? public_jwks : { keys: [] } } 70 | payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: key_loader }) 71 | expect(payload).to eq(token_payload) 72 | end 73 | end 74 | 75 | context 'when jwk keys are loaded from JSON with string keys' do 76 | it 'decodes the token' do 77 | key_loader = ->(_options) { JSON.parse(JSON.generate(public_jwks)) } 78 | payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: key_loader }) 79 | expect(payload).to eq(token_payload) 80 | end 81 | end 82 | 83 | context 'when the token kid is nil' do 84 | let(:token_headers) { {} } 85 | context 'and allow_nil_kid is specified' do 86 | it 'decodes the token' do 87 | key_loader = ->(_options) { JSON.parse(JSON.generate(public_jwks)) } 88 | payload, _header = described_class.decode(signed_token, nil, true, { algorithms: ['RS512'], jwks: key_loader, allow_nil_kid: true }) 89 | expect(payload).to eq(token_payload) 90 | end 91 | end 92 | end 93 | 94 | context 'when the token kid is not a string' do 95 | let(:token_headers) { { kid: 5 } } 96 | it 'raises an exception' do 97 | expect { described_class.decode(signed_token, nil, true, { algorithms: ['RS512'], jwks: public_jwks }) }.to raise_error( 98 | JWT::DecodeError, 'Invalid type for kid header parameter' 99 | ) 100 | end 101 | end 102 | 103 | context 'mixing algorithms using kid header' do 104 | let(:hmac_jwk) { JWT::JWK.new('secret') } 105 | let(:rsa_jwk) { JWT::JWK.new(test_pkey('rsa-2048-private.pem')) } 106 | let(:ec_jwk_secp384r1) { JWT::JWK.new(test_pkey('ec384-private.pem')) } 107 | let(:ec_jwk_secp521r1) { JWT::JWK.new(test_pkey('ec384-private.pem')) } 108 | let(:jwks) { { keys: [hmac_jwk.export(include_private: true), rsa_jwk.export, ec_jwk_secp384r1.export, ec_jwk_secp521r1.export] } } 109 | 110 | context 'when RSA key is pointed to as HMAC secret' do 111 | let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, 'is not really relevant in the scenario', 'HS256', { kid: rsa_jwk.kid }) } 112 | 113 | it 'raises JWT::DecodeError' do 114 | expect { described_class.decode(signed_token, nil, true, algorithms: ['HS256'], jwks: jwks) }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String') 115 | end 116 | end 117 | 118 | context 'when EC key is pointed to as HMAC secret' do 119 | let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, 'is not really relevant in the scenario', 'HS256', { kid: ec_jwk_secp384r1.kid }) } 120 | 121 | it 'raises JWT::DecodeError' do 122 | expect { described_class.decode(signed_token, nil, true, algorithms: ['HS256'], jwks: jwks) }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String') 123 | end 124 | end 125 | 126 | context 'when EC key is pointed to as RSA public key' do 127 | let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, rsa_jwk.signing_key, algorithm, { kid: ec_jwk_secp384r1.kid }) } 128 | 129 | it 'fails in some way' do 130 | expect { described_class.decode(signed_token, nil, true, algorithms: [algorithm], jwks: jwks) }.to( 131 | raise_error(JWT::VerificationError, 'Signature verification raised') 132 | ) 133 | end 134 | end 135 | 136 | context 'when HMAC secret is pointed to as RSA public key' do 137 | let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, rsa_jwk.signing_key, algorithm, { kid: hmac_jwk.kid }) } 138 | 139 | it 'fails in some way' do 140 | expect { described_class.decode(signed_token, nil, true, algorithms: [algorithm], jwks: jwks) }.to( 141 | raise_error(NoMethodError, /undefined method .*verify/) 142 | ) 143 | end 144 | end 145 | 146 | context 'when HMAC secret is pointed to as EC public key' do 147 | let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, ec_jwk_secp384r1.signing_key, 'ES384', { kid: hmac_jwk.kid }) } 148 | 149 | it 'fails in some way' do 150 | expect { described_class.decode(signed_token, nil, true, algorithms: ['ES384'], jwks: jwks) }.to( 151 | raise_error(NoMethodError, /undefined method .*group/) 152 | ) 153 | end 154 | end 155 | 156 | context 'when ES384 key is pointed to as ES512 key' do 157 | let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, ec_jwk_secp384r1.signing_key, 'ES512', { kid: ec_jwk_secp521r1.kid }) } 158 | 159 | it 'fails in some way' do 160 | expect { described_class.decode(signed_token, nil, true, algorithms: ['ES512'], jwks: jwks) }.to( 161 | raise_error(JWT::IncorrectAlgorithm, 'payload algorithm is ES512 but ES384 signing key was provided') 162 | ) 163 | end 164 | end 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /spec/jwt/jwk/ec_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::JWK::EC do 4 | let(:ec_key) { test_pkey('ec384-private.pem') } 5 | 6 | describe '.new' do 7 | subject { described_class.new(keypair) } 8 | 9 | context 'when a keypair with both keys given' do 10 | let(:keypair) { ec_key } 11 | it 'creates an instance of the class' do 12 | expect(subject).to be_a described_class 13 | expect(subject.private?).to eq true 14 | end 15 | end 16 | 17 | context 'when a keypair with only public key is given' do 18 | let(:keypair) { test_pkey('ec256-public.pem') } 19 | it 'creates an instance of the class' do 20 | expect(subject).to be_a described_class 21 | expect(subject.private?).to eq false 22 | end 23 | end 24 | 25 | context 'when a number is given' do 26 | let(:keypair) { 1234 } 27 | it 'raises an argument error' do 28 | expect { subject }.to raise_error(ArgumentError, 'key must be of type OpenSSL::PKey::EC or Hash with key parameters') 29 | end 30 | end 31 | 32 | context 'when EC with unsupported curve is given' do 33 | let(:keypair) { OpenSSL::PKey::EC.generate('prime239v2') } 34 | it 'raises an error' do 35 | expect { subject }.to raise_error(JWT::JWKError, "Unsupported curve 'prime239v2'") 36 | end 37 | end 38 | end 39 | 40 | describe '#keypair' do 41 | subject(:jwk) { described_class.new(ec_key) } 42 | 43 | it 'returns the key' do 44 | expect(jwk.keypair).to eq(ec_key) 45 | end 46 | end 47 | 48 | describe '#public_key' do 49 | subject(:jwk) { described_class.new(ec_key) } 50 | 51 | it 'returns the key' do 52 | expect(jwk.public_key).to eq(ec_key) 53 | end 54 | end 55 | 56 | describe '#export' do 57 | let(:kid) { nil } 58 | subject { described_class.new(keypair, kid).export } 59 | 60 | context 'when keypair with private key is exported' do 61 | let(:keypair) { ec_key } 62 | it 'returns a hash with the both parts of the key' do 63 | expect(subject).to be_a Hash 64 | expect(subject).to include(:kty, :kid, :x, :y) 65 | 66 | # Exported keys do not currently include private key info, 67 | # event if the in-memory key had that information. This is 68 | # done to match the traditional behavior of RSA JWKs. 69 | ## expect(subject).to include(:d) 70 | end 71 | end 72 | 73 | context 'when keypair with public key is exported' do 74 | let(:keypair) { test_pkey('ec256-public.pem') } 75 | it 'returns a hash with the public parts of the key' do 76 | expect(subject).to be_a Hash 77 | expect(subject).to include(:kty, :kid, :x, :y) 78 | 79 | # Don't include private `d` if not explicitly requested. 80 | expect(subject).not_to include(:d) 81 | end 82 | 83 | context 'when a custom "kid" is provided' do 84 | let(:kid) { 'custom_key_identifier' } 85 | it 'exports it' do 86 | expect(subject[:kid]).to eq 'custom_key_identifier' 87 | end 88 | end 89 | end 90 | 91 | context 'when private key is requested' do 92 | subject { described_class.new(keypair).export(include_private: true) } 93 | let(:keypair) { ec_key } 94 | it 'returns a hash with the both parts of the key' do 95 | expect(subject).to be_a Hash 96 | expect(subject).to include(:kty, :kid, :x, :y) 97 | 98 | # `d` is the private part. 99 | expect(subject).to include(:d) 100 | end 101 | end 102 | 103 | context 'when a common parameter is given' do 104 | let(:parameters) { { use: 'sig' } } 105 | let(:keypair) { ec_key } 106 | subject { described_class.new(keypair, parameters).export } 107 | it 'returns a hash including the common parameter' do 108 | expect(subject).to include(:use) 109 | end 110 | end 111 | end 112 | 113 | describe '.import' do 114 | subject { described_class.import(params) } 115 | let(:include_private) { false } 116 | let(:exported_key) { described_class.new(keypair).export(include_private: include_private) } 117 | 118 | %w[P-256 P-384 P-521 P-256K].each do |crv| 119 | context "when crv=#{crv}" do 120 | let(:openssl_curve) { JWT::JWK::EC.to_openssl_curve(crv) } 121 | let(:ec_key) { OpenSSL::PKey::EC.generate(openssl_curve) } 122 | 123 | context 'when keypair is private' do 124 | let(:include_private) { true } 125 | let(:keypair) { ec_key } 126 | let(:params) { exported_key } 127 | 128 | it 'returns a private key' do 129 | expect(subject.private?).to eq true 130 | expect(subject).to be_a described_class 131 | 132 | # Regular export returns only the non-private parts. 133 | public_only = exported_key.reject { |k, _v| k == :d } 134 | expect(subject.export).to eq(public_only) 135 | 136 | # Private export returns the original input. 137 | expect(subject.export(include_private: true)).to eq(exported_key) 138 | end 139 | 140 | context 'with a custom "kid" value' do 141 | let(:exported_key) do 142 | super().merge(kid: 'custom_key_identifier') 143 | end 144 | it 'imports that "kid" value' do 145 | expect(subject.kid).to eq('custom_key_identifier') 146 | end 147 | end 148 | end 149 | 150 | context 'when keypair is public' do 151 | context 'returns a public key' do 152 | let(:keypair) { test_pkey('ec256-public.pem') } 153 | let(:params) { exported_key } 154 | 155 | it 'returns a hash with the public parts of the key' do 156 | expect(subject).to be_a described_class 157 | expect(subject.private?).to eq false 158 | expect(subject.export).to eq(exported_key) 159 | end 160 | end 161 | end 162 | end 163 | 164 | context 'with missing 0-byte at the start of EC coordinates' do 165 | let(:example_keysets) do 166 | [ 167 | '{"kty":"EC","crv":"P-256","x":"0Nv5IKAlkvXuAKmOmFgmrwXKR7qGePOzu_7RXg5msw","y":"FqnPSNutcjfvXNlufwb7nLJuUEnBkbMdZ3P79nY9c3k"}', 168 | '{"kty":"EC","crv":"P-256","x":"xGjPg-7meZamM_yfkGeBUB2eJ5c82Y8vQdXwi5cVGw","y":"9FwKAuJacVyEy71yoVn1u1ETsQoiwF7QfkfXURGxg14"}', 169 | '{"kty":"EC","crv":"P-256","x":"yTvy0bwt5s29mIg1DMq-IjZH4pDgZIN9keEEaSuWZhk","y":"a0nrmd8qz8jpZDgpY82Rgv3vZ5xiJuiAoMIuRlGnaw"}', 170 | '{"kty":"EC","crv":"P-256","x":"yJen7AW4lLUTMH4luDj0wlMNSGCuOBB5R-ZoxlAU_g","y":"aMbA-M6ORHePSatiPVz_Pzu7z2XRnKMzK-HIscpfud8"}', 171 | '{"kty":"EC","crv":"P-256","x":"p_D00Z1ydC7mBIpSKPUUrzVzY9Fr5NMhhGfnf4P9guw","y":"lCqM3B_s04uhm7_91oycBvoWzuQWJCbMoZc46uqHXA"}', 172 | '{"kty":"EC","crv":"P-256","x":"hKS-vxV1bvfZ2xOuHv6Qt3lmHIiArTnhWac31kXw3w","y":"f_UWjrTpmq_oTdfss7YJ-9dEiYw_JC90kwAE-y0Yu-w"}', 173 | '{"kty":"EC","crv":"P-256","x":"3W22hN16OJN1XPpUQuCxtwoBRlf-wGyBNIihQiTmSdI","y":"eUaveaPQ4CpyfY7sfCqEF1DCOoxHdMpPHW15BmUF0w"}', 174 | '{"kty":"EC","crv":"P-256","x":"oq_00cGL3SxUZTA-JvcXALhfQya7elFuC7jcJScN7Bs","y":"1nNPIinv_gQiwStfx7vqs7Vt_MSyzoQDy9sCnZlFfg"}', 175 | '{"crv":"P-521","kty":"EC","x":"AMNQr/q+YGv4GfkEjrXH2N0+hnGes4cCqahJlV39m3aJpqSK+uiAvkRE5SDm2bZBc3YHGzhDzfMTUpnvXwjugUQP","y":"fIwouWsnp44Fjh2gBmO8ZafnpXZwLOCoaT5itu/Q4Z6j3duRfqmDsqyxZueDA3Gaac2LkbWGplT7mg4j7vCuGsw="}' 176 | ] 177 | end 178 | 179 | it 'prepends a 0-byte to either X or Y coordinate so that the keys decode correctly' do 180 | example_keysets.each do |keyset_json| 181 | jwk = described_class.import(JSON.parse(keyset_json)) 182 | expect(jwk).to be_kind_of(JWT::JWK::EC) 183 | end 184 | end 185 | end 186 | end 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /spec/jwt/jwk/hmac_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::JWK::HMAC do 4 | let(:hmac_key) { 'secret-key' } 5 | let(:key) { hmac_key } 6 | subject(:jwk) { described_class.new(key) } 7 | 8 | describe '.new' do 9 | context 'when a secret key given' do 10 | it 'creates an instance of the class' do 11 | expect(jwk).to be_a described_class 12 | expect(jwk.private?).to eq true 13 | end 14 | end 15 | 16 | context 'when key is a number' do 17 | let(:key) { 123 } 18 | it 'raises an ArgumentError' do 19 | expect { jwk }.to raise_error(ArgumentError, 'key must be of type String or Hash with key parameters') 20 | end 21 | end 22 | end 23 | 24 | describe '#keypair' do 25 | it 'returns a string' do 26 | expect(jwk.keypair).to eq(key) 27 | end 28 | end 29 | 30 | describe '#export' do 31 | let(:kid) { nil } 32 | 33 | context 'when key is exported' do 34 | let(:key) { hmac_key } 35 | subject { described_class.new(key, kid).export } 36 | it 'returns a hash with the key' do 37 | expect(subject).to be_a Hash 38 | expect(subject).to include(:kty, :kid) 39 | end 40 | end 41 | 42 | context 'when key is exported with private key' do 43 | let(:key) { hmac_key } 44 | subject { described_class.new(key, kid).export(include_private: true) } 45 | it 'returns a hash with the key' do 46 | expect(subject).to be_a Hash 47 | expect(subject).to include(:kty, :kid, :k) 48 | end 49 | end 50 | end 51 | 52 | describe '.import' do 53 | subject { described_class.import(params) } 54 | let(:exported_key) { described_class.new(key).export(include_private: true) } 55 | 56 | context 'when secret key is given' do 57 | let(:key) { hmac_key } 58 | let(:params) { exported_key } 59 | 60 | it 'returns a key' do 61 | expect(subject).to be_a described_class 62 | expect(subject.export(include_private: true)).to eq(exported_key) 63 | end 64 | 65 | context 'with a custom "kid" value' do 66 | let(:exported_key) do 67 | super().merge(kid: 'custom_key_identifier') 68 | end 69 | it 'imports that "kid" value' do 70 | expect(subject.kid).to eq('custom_key_identifier') 71 | end 72 | end 73 | 74 | context 'with a common parameter' do 75 | let(:exported_key) do 76 | super().merge(use: 'sig') 77 | end 78 | it 'imports that common parameter' do 79 | expect(subject[:use]).to eq('sig') 80 | end 81 | end 82 | end 83 | 84 | context 'when example from RFC' do 85 | let(:params) { { kty: 'oct', k: 'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow' } } 86 | 87 | it 'decodes the k' do 88 | 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') 89 | expect(subject.verify_key).to eq(expected_key) 90 | end 91 | end 92 | end 93 | 94 | describe '#[]=' do 95 | context 'when k is given' do 96 | it 'raises an error' do 97 | expect { jwk[:k] = 'new_secret' }.to raise_error(ArgumentError, 'cannot overwrite cryptographic key attributes') 98 | end 99 | end 100 | end 101 | 102 | describe '#==' do 103 | it 'is equal to itself' do 104 | other = jwk 105 | expect(jwk == other).to eq true 106 | end 107 | 108 | it 'is equal to a clone of itself' do 109 | other = jwk.clone 110 | expect(jwk == other).to eq true 111 | end 112 | 113 | it 'is not equal to nil' do 114 | other = nil 115 | expect(jwk == other).to eq false 116 | end 117 | 118 | it 'is not equal to boolean true' do 119 | other = true 120 | expect(jwk == other).to eq false 121 | end 122 | 123 | it 'is not equal to a non-key' do 124 | other = Object.new 125 | expect(jwk == other).to eq false 126 | end 127 | 128 | it 'is not equal to a different key' do 129 | other = described_class.new('other-key') 130 | expect(jwk == other).to eq false 131 | end 132 | end 133 | 134 | describe '#<=>' do 135 | it 'is equal to itself' do 136 | other = jwk 137 | expect(jwk <=> other).to eq 0 138 | end 139 | 140 | it 'is equal to a clone of itself' do 141 | other = jwk.clone 142 | expect(jwk <=> other).to eq 0 143 | end 144 | 145 | it 'is not comparable to nil' do 146 | other = nil 147 | expect(jwk <=> other).to eq nil 148 | end 149 | 150 | it 'is not comparable to boolean true' do 151 | other = true 152 | expect(jwk <=> other).to eq nil 153 | end 154 | 155 | it 'is not comparable to a non-key' do 156 | other = Object.new 157 | expect(jwk <=> other).to eq nil 158 | end 159 | 160 | it 'is not equal to a different key' do 161 | other = described_class.new('other-key') 162 | expect(jwk <=> other).not_to eq 0 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /spec/jwt/jwk/rsa_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::JWK::RSA do 4 | let(:rsa_key) { OpenSSL::PKey::RSA.new(2048) } 5 | 6 | describe '.new' do 7 | subject { described_class.new(keypair) } 8 | 9 | context 'when a keypair with both keys given' do 10 | let(:keypair) { rsa_key } 11 | it 'creates an instance of the class' do 12 | expect(subject).to be_a described_class 13 | expect(subject.private?).to eq true 14 | end 15 | end 16 | 17 | context 'when a keypair with only public key is given' do 18 | let(:keypair) { rsa_key.public_key } 19 | it 'creates an instance of the class' do 20 | expect(subject).to be_a described_class 21 | expect(subject.private?).to eq false 22 | end 23 | end 24 | end 25 | 26 | describe '#keypair' do 27 | subject(:jwk) { described_class.new(rsa_key) } 28 | 29 | it 'warns to stderr' do 30 | expect(jwk.keypair).to eq(rsa_key) 31 | end 32 | end 33 | 34 | describe '#export' do 35 | subject { described_class.new(keypair).export } 36 | 37 | context 'when keypair with private key is exported' do 38 | let(:keypair) { rsa_key } 39 | it 'returns a hash with the public parts of the key' do 40 | expect(subject).to be_a Hash 41 | expect(subject).to include(:kty, :n, :e, :kid) 42 | expect(subject).not_to include(:d, :p, :dp, :dq, :qi) 43 | end 44 | end 45 | 46 | context 'when keypair with public key is exported' do 47 | let(:keypair) { rsa_key.public_key } 48 | it 'returns a hash with the public parts of the key' do 49 | expect(subject).to be_a Hash 50 | expect(subject).to include(:kty, :n, :e, :kid) 51 | expect(subject).not_to include(:d, :p, :dp, :dq, :qi) 52 | end 53 | end 54 | 55 | context 'when unsupported keypair is given' do 56 | let(:keypair) { 'key' } 57 | it 'raises an error' do 58 | expect { subject }.to raise_error(ArgumentError) 59 | end 60 | end 61 | 62 | context 'when private key is requested' do 63 | subject { described_class.new(keypair).export(include_private: true) } 64 | let(:keypair) { rsa_key } 65 | it 'returns a hash with the public AND private parts of the key' do 66 | expect(subject).to be_a Hash 67 | expect(subject).to include(:kty, :n, :e, :kid, :d, :p, :q, :dp, :dq, :qi) 68 | end 69 | end 70 | end 71 | 72 | describe '.kid' do 73 | context 'when configuration says to use :rfc7638_thumbprint' do 74 | before do 75 | JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint 76 | end 77 | 78 | it 'generates the kid based on the thumbprint' do 79 | expect(described_class.new(OpenSSL::PKey::RSA.new(2048)).kid.size).to eq(43) 80 | end 81 | end 82 | 83 | context 'when kid is given as a String parameter' do 84 | it 'uses the given kid' do 85 | expect(described_class.new(OpenSSL::PKey::RSA.new(2048), 'given').kid).to eq('given') 86 | end 87 | end 88 | 89 | context 'when kid is given in a hash parameter' do 90 | it 'uses the given kid' do 91 | expect(described_class.new(OpenSSL::PKey::RSA.new(2048), kid: 'given').kid).to eq('given') 92 | end 93 | end 94 | end 95 | 96 | describe '.common_parameters' do 97 | context 'when a common parameters hash is given' do 98 | it 'imports the common parameter' do 99 | expect(described_class.new(OpenSSL::PKey::RSA.new(2048), use: 'sig')[:use]).to eq('sig') 100 | end 101 | 102 | it 'converts string keys to symbol keys' do 103 | expect(described_class.new(OpenSSL::PKey::RSA.new(2048), { 'use' => 'sig' })[:use]).to eq('sig') 104 | end 105 | end 106 | end 107 | 108 | describe '.import' do 109 | subject { described_class.import(params) } 110 | let(:exported_key) { described_class.new(rsa_key).export } 111 | 112 | context 'when keypair is imported with symbol keys' do 113 | let(:params) { { kty: 'RSA', e: exported_key[:e], n: exported_key[:n] } } 114 | it 'returns a hash with the public parts of the key' do 115 | expect(subject).to be_a described_class 116 | expect(subject.private?).to eq false 117 | expect(subject.export).to eq(exported_key) 118 | end 119 | end 120 | 121 | context 'when keypair is imported with string keys from JSON' do 122 | let(:params) { { 'kty' => 'RSA', 'e' => exported_key[:e], 'n' => exported_key[:n] } } 123 | it 'returns a hash with the public parts of the key' do 124 | expect(subject).to be_a described_class 125 | expect(subject.private?).to eq false 126 | expect(subject.export).to eq(exported_key) 127 | end 128 | end 129 | 130 | context 'when private key is included in the data' do 131 | let(:exported_key) { described_class.new(rsa_key).export(include_private: true) } 132 | let(:params) { exported_key } 133 | it 'creates a complete keypair' do 134 | expect(subject).to be_a described_class 135 | expect(subject.private?).to eq true 136 | end 137 | end 138 | 139 | context 'when jwk_data is given without e and/or n' do 140 | let(:params) { { kty: 'RSA' } } 141 | it 'raises an error' do 142 | expect { subject }.to raise_error(JWT::JWKError, 'Key format is invalid for RSA') 143 | end 144 | end 145 | end 146 | 147 | shared_examples 'creating an RSA object from complete JWK parameters' do 148 | let(:rsa_parameters) { jwk_parameters.transform_values { |value| described_class.decode_open_ssl_bn(value) } } 149 | let(:all_jwk_parameters) { described_class.new(rsa_key).export(include_private: true) } 150 | 151 | context 'when public parameters (e, n) are given' do 152 | let(:jwk_parameters) { all_jwk_parameters.slice(:e, :n) } 153 | 154 | it 'creates a valid RSA object representing a public key' do 155 | expect(subject).to be_a(OpenSSL::PKey::RSA) 156 | expect(subject.private?).to eq(false) 157 | end 158 | end 159 | 160 | context 'when only e, n, d, p and q are given' do 161 | let(:jwk_parameters) { all_jwk_parameters.slice(:e, :n, :d, :p, :q) } 162 | 163 | it 'raises an error telling all the exponents are required' do 164 | 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') 165 | end 166 | end 167 | 168 | context 'when all key components n, e, d, p, q, dp, dq, qi are given' do 169 | let(:jwk_parameters) { all_jwk_parameters.slice(:n, :e, :d, :p, :q, :dp, :dq, :qi) } 170 | 171 | it 'creates a valid RSA object representing a public key' do 172 | expect(subject).to be_a(OpenSSL::PKey::RSA) 173 | expect(subject.private?).to eq(true) 174 | end 175 | end 176 | end 177 | 178 | shared_examples 'creating an RSA object from partial JWK parameters' do 179 | context 'when e, n, d is given' do 180 | let(:jwk_parameters) { all_jwk_parameters.slice(:e, :n, :d) } 181 | 182 | before do 183 | skip 'OpenSSL prior to 2.2 does not seem to support partial parameters' if JWT.openssl_version < Gem::Version.new('2.2') 184 | end 185 | 186 | it 'creates a valid RSA object representing a private key' do 187 | expect(subject).to be_a(OpenSSL::PKey::RSA) 188 | expect(subject.private?).to eq(true) 189 | end 190 | 191 | it 'can be used for encryption and decryption' do 192 | expect(subject.private_decrypt(subject.public_encrypt('secret'))).to eq('secret') 193 | end 194 | 195 | it 'can be used for signing and verification' do 196 | data = 'data_to_sign' 197 | signature = subject.sign(OpenSSL::Digest.new('SHA512'), data) 198 | expect(subject.verify(OpenSSL::Digest.new('SHA512'), signature, data)).to eq(true) 199 | end 200 | end 201 | end 202 | 203 | describe '.create_rsa_key_using_der' do 204 | subject(:rsa) { described_class.create_rsa_key_using_der(rsa_parameters) } 205 | 206 | include_examples 'creating an RSA object from complete JWK parameters' 207 | 208 | context 'when e, n, d is given' do 209 | let(:jwk_parameters) { all_jwk_parameters.slice(:e, :n, :d) } 210 | 211 | it 'expects all CRT parameters given and raises error' do 212 | expect { subject }.to raise_error(JWT::JWKError, 'Creating a RSA key with a private key requires the CRT parameters to be defined') 213 | end 214 | end 215 | end 216 | 217 | describe '.create_rsa_key_using_sets' do 218 | before do 219 | skip 'OpenSSL without the RSA#set_key method not supported' unless OpenSSL::PKey::RSA.new.respond_to?(:set_key) 220 | skip 'OpenSSL 3.0 does not allow mutating objects anymore' if JWT.openssl_3? 221 | end 222 | 223 | subject(:rsa) { described_class.create_rsa_key_using_sets(rsa_parameters) } 224 | 225 | include_examples 'creating an RSA object from complete JWK parameters' 226 | include_examples 'creating an RSA object from partial JWK parameters' 227 | end 228 | 229 | describe '.create_rsa_key_using_accessors' do 230 | before do 231 | skip 'OpenSSL if RSA#d= is not available there is no accessors anymore' unless OpenSSL::PKey::RSA.new.respond_to?(:d=) 232 | end 233 | 234 | subject(:rsa) { described_class.create_rsa_key_using_accessors(rsa_parameters) } 235 | 236 | include_examples 'creating an RSA object from complete JWK parameters' 237 | include_examples 'creating an RSA object from partial JWK parameters' 238 | end 239 | end 240 | -------------------------------------------------------------------------------- /spec/jwt/jwk/set_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::JWK::Set do 4 | describe '.new' do 5 | it 'can create an empty set' do 6 | expect(described_class.new.keys).to eql([]) 7 | end 8 | 9 | context 'can create a set' do 10 | it 'from a JWK' do 11 | jwk = JWT::JWK.new 'testkey' 12 | expect(described_class.new(jwk).keys).to eql([jwk]) 13 | end 14 | 15 | it 'from a JWKS hash with symbol keys' do 16 | jwks = { keys: [{ kty: 'oct', k: Base64.strict_encode64('testkey') }] } 17 | jwk = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) 18 | expect(described_class.new(jwks).keys).to eql([jwk]) 19 | end 20 | 21 | it 'from a JWKS hash with string keys' do 22 | jwks = { 'keys' => [{ 'kty' => 'oct', 'k' => Base64.strict_encode64('testkey') }] } 23 | jwk = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) 24 | expect(described_class.new(jwks).keys).to eql([jwk]) 25 | end 26 | 27 | it 'from an array of keys' do 28 | jwk = JWT::JWK.new 'testkey' 29 | expect(described_class.new([jwk]).keys).to eql([jwk]) 30 | end 31 | 32 | it 'from an existing JWT::JWK::Set' do 33 | jwk = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) 34 | jwks = described_class.new(jwk) 35 | expect(described_class.new(jwks)).to eql(jwks) 36 | end 37 | end 38 | 39 | it 'raises an error on invalid inputs' do 40 | expect { described_class.new(42) }.to raise_error(ArgumentError) 41 | end 42 | end 43 | 44 | describe '.export' do 45 | it 'exports the JWKS to Hash' do 46 | jwk = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) 47 | jwks = described_class.new(jwk) 48 | exported = jwks.export 49 | expect(exported[:keys].size).to eql(1) 50 | expect(exported[:keys][0]).to eql(jwk.export) 51 | end 52 | end 53 | 54 | describe '.eql?' do 55 | it 'correctly classifies equal sets' do 56 | jwk = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) 57 | jwks1 = described_class.new(jwk) 58 | jwks2 = described_class.new(jwk) 59 | expect(jwks1).to eql(jwks2) 60 | end 61 | 62 | it 'correctly classifies different sets' do 63 | jwk1 = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) 64 | jwk2 = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkex') }) 65 | jwks1 = described_class.new(jwk1) 66 | jwks2 = described_class.new(jwk2) 67 | expect(jwks1).not_to eql(jwks2) 68 | end 69 | end 70 | 71 | # TODO: No idea why this does not work. eql? returns true for the two elements, 72 | # but Array#uniq! doesn't recognize this, despite the documentation saying otherwise 73 | describe '.uniq!' do 74 | it 'filters out equal keys' do 75 | jwk = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) 76 | jwk2 = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) 77 | jwks = described_class.new([jwk, jwk2]) 78 | jwks.uniq! 79 | expect(jwks.keys.size).to eql(1) 80 | end 81 | end 82 | 83 | describe '.select!' do 84 | it 'filters the keyset' do 85 | jwks = described_class.new([]) 86 | jwks << JWT::JWK.new(test_pkey('rsa-2048-private.pem')) 87 | jwks << JWT::JWK.new(test_pkey('ec384-private.pem')) 88 | jwks.select! { |k| k[:kty] == 'RSA' } 89 | expect(jwks.size).to eql(1) 90 | expect(jwks.keys[0][:kty]).to eql('RSA') 91 | end 92 | end 93 | 94 | describe '.reject!' do 95 | it 'filters the keyset' do 96 | jwks = described_class.new([]) 97 | jwks << JWT::JWK.new(test_pkey('rsa-2048-private.pem')) 98 | jwks << JWT::JWK.new(test_pkey('ec384-private.pem')) 99 | jwks.reject! { |k| k[:kty] == 'RSA' } 100 | expect(jwks.size).to eql(1) 101 | expect(jwks.keys[0][:kty]).to eql('EC') 102 | end 103 | end 104 | 105 | describe '.merge' do 106 | context 'merges two JWKSs' do 107 | it 'when called via .union' do 108 | jwks1 = described_class.new(JWT::JWK.new(test_pkey('rsa-2048-private.pem'))) 109 | jwks2 = described_class.new(JWT::JWK.new(test_pkey('ec384-private.pem'))) 110 | jwks3 = jwks1.union(jwks2) 111 | expect(jwks1.size).to eql(1) 112 | expect(jwks2.size).to eql(1) 113 | expect(jwks3.size).to eql(2) 114 | expect(jwks3.keys).to include(jwks1.keys[0]) 115 | expect(jwks3.keys).to include(jwks2.keys[0]) 116 | end 117 | 118 | it 'when called via "|" operator' do 119 | jwks1 = described_class.new(JWT::JWK.new(test_pkey('rsa-2048-private.pem'))) 120 | jwks2 = described_class.new(JWT::JWK.new(test_pkey('ec384-private.pem'))) 121 | jwks3 = jwks1 | jwks2 122 | expect(jwks1.size).to eql(1) 123 | expect(jwks2.size).to eql(1) 124 | expect(jwks3.size).to eql(2) 125 | expect(jwks3.keys).to include(jwks1.keys[0]) 126 | expect(jwks3.keys).to include(jwks2.keys[0]) 127 | end 128 | 129 | it 'when called directly' do 130 | jwks1 = described_class.new(JWT::JWK.new(test_pkey('rsa-2048-private.pem'))) 131 | jwks2 = described_class.new(JWT::JWK.new(test_pkey('ec384-private.pem'))) 132 | jwks3 = jwks1.merge(jwks2) 133 | expect(jwks1.size).to eql(2) 134 | expect(jwks2.size).to eql(1) 135 | expect(jwks3).to eql(jwks1) 136 | expect(jwks3.keys).to include(jwks2.keys[0]) 137 | end 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /spec/jwt/jwk/thumbprint_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe JWT::JWK::Thumbprint do 4 | describe '#to_s' do 5 | let(:jwk_json) { nil } 6 | let(:jwk) { JWT::JWK.import(JSON.parse(jwk_json)) } 7 | 8 | subject(:thumbprint) { described_class.new(jwk).to_s } 9 | 10 | context 'when example from RFC is given' do 11 | let(:jwk_json) do 12 | ' 13 | { 14 | "kty": "RSA", 15 | "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt' \ 16 | 'VT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn6' \ 17 | '4tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FD' \ 18 | 'W2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n9' \ 19 | '1CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINH' \ 20 | 'aQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", 21 | "e": "AQAB", 22 | "alg": "RS256" 23 | } 24 | ' 25 | end 26 | 27 | it { is_expected.to eq('NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs') } 28 | end 29 | 30 | context 'when HMAC key is given' do 31 | let(:jwk_json) do 32 | ' 33 | { 34 | "kty":"oct", 35 | "alg":"HS512", 36 | "k":"B4uZ7IbZTnjdCQjUBXTpzMUznCYj3wdYDZcceeU0mLg" 37 | } 38 | ' 39 | end 40 | 41 | it { is_expected.to eq('wPf4ZF5qlzoFxsGkft4eu1iWcehgAcahZL4XPV4dT-s') } 42 | end 43 | 44 | context 'when EC key is given' do 45 | let(:jwk_json) do 46 | ' 47 | { 48 | "kty":"EC", 49 | "crv":"P-384", 50 | "x":"sbOnPOXPBULpeizfstr8b6b31QmvEnChXJNYBhXlmpGbs3vZtomBxNORYTT9Wylq", 51 | "y":"mfyY4VJDbdKGVjBSIhN9BJEq--6IPuKy3gbIr734n6Xd81lnvKslPwjB-sdGouD6" 52 | } 53 | ' 54 | end 55 | 56 | it { is_expected.to eq('dO52_we59sdR49HsGCpVzlDUQNvT3KxCTGakk4Un8qc') } 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/jwt/jwk_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::JWK do 4 | let(:rsa_key) { test_pkey('rsa-2048-private.pem') } 5 | let(:ec_key) { test_pkey('ec256k-private.pem') } 6 | 7 | describe '.import' do 8 | let(:keypair) { rsa_key.public_key } 9 | let(:exported_key) { described_class.new(keypair).export } 10 | let(:params) { exported_key } 11 | 12 | subject { described_class.import(params) } 13 | 14 | it 'creates a ::JWT::JWK::RSA instance' do 15 | expect(subject).to be_a JWT::JWK::RSA 16 | expect(subject.export).to eq(exported_key) 17 | end 18 | 19 | context 'when number is given' do 20 | let(:params) { 1234 } 21 | it 'raises an error' do 22 | expect { subject }.to raise_error(JWT::JWKError, 'Cannot create JWK from a Integer') 23 | end 24 | end 25 | 26 | context 'parsed from JSON' do 27 | let(:params) { exported_key } 28 | it 'creates a ::JWT::JWK::RSA instance from JSON parsed JWK' do 29 | expect(subject).to be_a JWT::JWK::RSA 30 | expect(subject.export).to eq(exported_key) 31 | end 32 | end 33 | 34 | context 'when keytype is not supported' do 35 | let(:params) { { kty: 'unsupported' } } 36 | 37 | it 'raises an error' do 38 | expect { subject }.to raise_error(JWT::JWKError) 39 | end 40 | end 41 | 42 | context 'when keypair with defined kid is imported' do 43 | it 'returns the predefined kid if jwt_data contains a kid' do 44 | params[:kid] = 'CUSTOM_KID' 45 | expect(subject.export).to eq(params) 46 | end 47 | end 48 | 49 | context 'when a common JWK parameter is specified' do 50 | it 'returns the defined common JWK parameter' do 51 | params[:use] = 'sig' 52 | expect(subject.export).to eq(params) 53 | end 54 | end 55 | end 56 | 57 | describe '.new' do 58 | let(:options) { nil } 59 | subject { described_class.new(keypair, options) } 60 | 61 | context 'when RSA key is given' do 62 | let(:keypair) { rsa_key } 63 | it { is_expected.to be_a JWT::JWK::RSA } 64 | end 65 | 66 | context 'when secret key is given' do 67 | let(:keypair) { 'secret-key' } 68 | it { is_expected.to be_a JWT::JWK::HMAC } 69 | end 70 | 71 | context 'when EC key is given' do 72 | let(:keypair) { ec_key } 73 | it { is_expected.to be_a JWT::JWK::EC } 74 | end 75 | 76 | context 'when kid is given' do 77 | let(:keypair) { rsa_key } 78 | let(:options) { 'CUSTOM_KID' } 79 | it 'sets the kid' do 80 | expect(subject.kid).to eq(options) 81 | end 82 | end 83 | 84 | context 'when a common parameter is given' do 85 | subject { described_class.new(keypair, params) } 86 | let(:keypair) { rsa_key } 87 | let(:params) { { 'use' => 'sig' } } 88 | it 'sets the common parameter' do 89 | expect(subject[:use]).to eq('sig') 90 | end 91 | end 92 | end 93 | 94 | describe '.[]' do 95 | let(:params) { { use: 'sig' } } 96 | let(:keypair) { rsa_key } 97 | subject { described_class.new(keypair, params) } 98 | 99 | it 'allows to read common parameters via the key-accessor' do 100 | expect(subject[:use]).to eq('sig') 101 | end 102 | 103 | it 'allows to set common parameters via the key-accessor' do 104 | subject[:use] = 'enc' 105 | expect(subject[:use]).to eq('enc') 106 | end 107 | 108 | it 'rejects key parameters as keys via the key-accessor' do 109 | expect { subject[:kty] = 'something' }.to raise_error(ArgumentError) 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/jwt/token_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::Token do 4 | let(:payload) { { 'pay' => 'load' } } 5 | let(:header) { {} } 6 | 7 | subject(:token) { described_class.new(payload: payload, header: header) } 8 | 9 | describe '#sign!' do 10 | it 'signs the token' do 11 | token.sign!(algorithm: 'HS256', key: 'secret') 12 | 13 | expect(JWT::EncodedToken.new(token.jwt).valid_signature?(algorithm: 'HS256', key: 'secret')).to be(true) 14 | end 15 | 16 | context 'when signed twice' do 17 | before do 18 | token.sign!(algorithm: 'HS256', key: 'secret') 19 | end 20 | 21 | it 'raises' do 22 | expect { token.sign!(algorithm: 'HS256', key: 'secret') }.to raise_error(JWT::EncodeError) 23 | end 24 | end 25 | end 26 | 27 | describe '#jwt' do 28 | context 'when token is signed' do 29 | before do 30 | token.sign!(algorithm: 'HS256', key: 'secret') 31 | end 32 | 33 | it 'returns a signed and encoded token' do 34 | expect(token.jwt).to eq('eyJhbGciOiJIUzI1NiJ9.eyJwYXkiOiJsb2FkIn0.UEhDY1Qlj29ammxuVRA_-gBah4qTy5FngIWg0yEAlC0') 35 | expect(JWT.decode(token.jwt, 'secret', true, algorithm: 'HS256')).to eq([{ 'pay' => 'load' }, { 'alg' => 'HS256' }]) 36 | end 37 | end 38 | 39 | context 'when token is not signed' do 40 | it 'returns a signed and encoded token' do 41 | expect { token.jwt }.to raise_error(JWT::EncodeError) 42 | end 43 | end 44 | 45 | context 'when alg is given in header' do 46 | let(:header) { { 'alg' => 'HS123' } } 47 | 48 | before do 49 | token.sign!(algorithm: 'HS256', key: 'secret') 50 | end 51 | 52 | it 'returns a signed and encoded token' do 53 | expect(JWT::EncodedToken.new(token.jwt).header).to eq({ 'alg' => 'HS123' }) 54 | end 55 | end 56 | end 57 | 58 | describe '#detach_payload!' do 59 | context 'before token is signed' do 60 | it 'detaches the payload' do 61 | token.detach_payload! 62 | token.sign!(algorithm: 'HS256', key: 'secret') 63 | expect(token.jwt).to eq('eyJhbGciOiJIUzI1NiJ9..UEhDY1Qlj29ammxuVRA_-gBah4qTy5FngIWg0yEAlC0') 64 | end 65 | end 66 | end 67 | 68 | describe '#verify_claims!' do 69 | context 'when required_claims is passed' do 70 | it 'raises error' do 71 | expect { token.verify_claims!(required: ['exp']) }.to raise_error(JWT::MissingRequiredClaim, 'Missing required claim exp') 72 | end 73 | end 74 | end 75 | 76 | describe '#valid_claims?' do 77 | context 'exp claim' do 78 | let(:payload) { { 'exp' => Time.now.to_i - 10, 'pay' => 'load' } } 79 | 80 | context 'when claim is valid' do 81 | it 'returns true' do 82 | expect(token.valid_claims?(exp: { leeway: 1000 })).to be(true) 83 | end 84 | end 85 | 86 | context 'when claim is invalid' do 87 | it 'returns true' do 88 | expect(token.valid_claims?(:exp)).to be(false) 89 | end 90 | end 91 | end 92 | end 93 | 94 | describe '#claim_errors' do 95 | context 'exp claim' do 96 | let(:payload) { { 'exp' => Time.now.to_i - 10, 'pay' => 'load' } } 97 | 98 | context 'when claim is valid' do 99 | it 'returns empty array' do 100 | expect(token.claim_errors(exp: { leeway: 1000 })).to be_empty 101 | end 102 | end 103 | 104 | context 'when claim is invalid' do 105 | it 'returns array with error objects' do 106 | expect(token.claim_errors(:exp).map(&:message)).to eq(['Signature has expired']) 107 | end 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/jwt/version_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT do 4 | describe '.gem_version' do 5 | it 'returns the gem version' do 6 | expect(described_class.gem_version).to eq(Gem::Version.new(JWT::VERSION::STRING)) 7 | end 8 | end 9 | describe 'VERSION constants' do 10 | it 'has a MAJOR version' do 11 | expect(JWT::VERSION::MAJOR).to be_a(Integer) 12 | end 13 | 14 | it 'has a MINOR version' do 15 | expect(JWT::VERSION::MINOR).to be_a(Integer) 16 | end 17 | 18 | it 'has a TINY version' do 19 | expect(JWT::VERSION::TINY).to be_a(Integer) 20 | end 21 | 22 | it 'has a PRE version' do 23 | expect(JWT::VERSION::PRE).to be_a(String).or be_nil 24 | end 25 | 26 | it 'has a STRING version' do 27 | expect(JWT::VERSION::STRING).to be_a(String) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/jwt/x5c_key_finder_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe JWT::X5cKeyFinder do 4 | let(:root_key) { test_pkey('rsa-2048-private.pem') } 5 | let(:root_dn) { OpenSSL::X509::Name.parse('/DC=org/DC=fake-ca/CN=Fake CA') } 6 | let(:root_certificate) { generate_root_cert(root_dn, root_key) } 7 | let(:leaf_key) { generate_key } 8 | let(:leaf_dn) { OpenSSL::X509::Name.parse('/DC=org/DC=fake/CN=Fake') } 9 | let(:leaf_serial) { 2 } 10 | let(:leaf_not_after) { Time.now + 3600 } 11 | let(:leaf_signing_key) { root_key } 12 | let(:leaf_certificate) do 13 | cert = generate_cert( 14 | leaf_dn, 15 | leaf_key.public_key, 16 | leaf_serial, 17 | issuer: root_certificate, 18 | not_after: leaf_not_after 19 | ) 20 | ef = OpenSSL::X509::ExtensionFactory.new 21 | ef.config = OpenSSL::Config.parse(leaf_cdp) 22 | ef.subject_certificate = cert 23 | cert.add_extension(ef.create_extension('crlDistributionPoints', '@crlDistPts')) 24 | cert.sign(leaf_signing_key, 'sha256') 25 | cert 26 | end 27 | let(:leaf_cdp) { <<-_CNF_ } 28 | [crlDistPts] 29 | URI.1 = http://www.example.com/crl 30 | _CNF_ 31 | 32 | let(:crl) { issue_crl([], issuer: root_certificate, issuer_key: root_key) } 33 | 34 | let(:x5c_header) { [Base64.strict_encode64(leaf_certificate.to_der)] } 35 | subject(:keyfinder) { described_class.new([root_certificate], [crl]).from(x5c_header) } 36 | 37 | it 'returns the public key from a certificate that is signed by trusted roots and not revoked' do 38 | expect(keyfinder).to be_a(OpenSSL::PKey::RSA) 39 | expect(keyfinder.public_key.to_der).to eq(leaf_certificate.public_key.to_der) 40 | end 41 | 42 | context 'already parsed certificates' do 43 | let(:x5c_header) { [leaf_certificate] } 44 | 45 | it 'returns the public key from a certificate that is signed by trusted roots and not revoked' do 46 | expect(keyfinder).to be_a(OpenSSL::PKey::RSA) 47 | expect(keyfinder.public_key.to_der).to eq(leaf_certificate.public_key.to_der) 48 | end 49 | end 50 | 51 | context '::JWT.decode' do 52 | let(:token_payload) { { 'data' => 'something' } } 53 | let(:encoded_token) { JWT.encode(token_payload, leaf_key, 'RS256', { 'x5c' => x5c_header }) } 54 | let(:decoded_payload) do 55 | JWT.decode(encoded_token, nil, true, algorithms: ['RS256'], x5c: { root_certificates: [root_certificate], crls: [crl] }).first 56 | end 57 | 58 | it 'returns the encoded payload after successful certificate path verification' do 59 | expect(decoded_payload).to eq(token_payload) 60 | end 61 | end 62 | 63 | context 'certificate' do 64 | context 'expired' do 65 | let(:leaf_not_after) { Time.now - 3600 } 66 | 67 | it 'raises an error' do 68 | error = 'Certificate verification failed: certificate has expired. Certificate subject: /DC=org/DC=fake/CN=Fake.' 69 | expect { keyfinder }.to raise_error(JWT::VerificationError, error) 70 | end 71 | end 72 | 73 | context 'signature could not be verified with the given trusted roots' do 74 | let(:leaf_signing_key) { generate_key } 75 | 76 | it 'raises an error' do 77 | error = 'Certificate verification failed: certificate signature failure. Certificate subject: /DC=org/DC=fake/CN=Fake.' 78 | expect { keyfinder }.to raise_error(JWT::VerificationError, error) 79 | end 80 | end 81 | 82 | context 'could not be chained to a trusted root certificate' do 83 | context 'given an array' do 84 | subject(:keyfinder) { described_class.new([], [crl]).from(x5c_header) } 85 | 86 | it 'raises a verification error' do 87 | error = 'Certificate verification failed: unable to get local issuer certificate. Certificate subject: /DC=org/DC=fake/CN=Fake.' 88 | expect { keyfinder }.to raise_error(JWT::VerificationError, error) 89 | end 90 | end 91 | 92 | context 'given nil' do 93 | subject(:keyfinder) { described_class.new(nil, [crl]).from(x5c_header) } 94 | 95 | it 'raises a decode error' do 96 | error = 'Root certificates must be specified' 97 | expect { keyfinder }.to raise_error(ArgumentError, error) 98 | end 99 | end 100 | end 101 | 102 | context 'revoked' do 103 | let(:revocation) { [leaf_serial, Time.now - 60, 1] } 104 | let(:crl) { issue_crl([revocation], issuer: root_certificate, issuer_key: root_key) } 105 | 106 | it 'raises an error' do 107 | error = 'Certificate verification failed: certificate revoked. Certificate subject: /DC=org/DC=fake/CN=Fake.' 108 | expect { keyfinder }.to raise_error(JWT::VerificationError, error) 109 | end 110 | end 111 | end 112 | 113 | context 'CRL' do 114 | context 'expired' do 115 | let(:next_up) { Time.now - 60 } 116 | let(:crl) { issue_crl([], next_up: next_up, issuer: root_certificate, issuer_key: root_key) } 117 | 118 | it 'raises an error' do 119 | error = 'Certificate verification failed: CRL has expired. Certificate subject: /DC=org/DC=fake/CN=Fake.' 120 | expect { keyfinder }.to raise_error(JWT::VerificationError, error) 121 | end 122 | end 123 | 124 | context 'signature could not be verified with the given trusted roots' do 125 | let(:crl) { issue_crl([], issuer: root_certificate, issuer_key: generate_key) } 126 | 127 | it 'raises an error' do 128 | error = 'Certificate verification failed: CRL signature failure. Certificate subject: /DC=org/DC=fake/CN=Fake.' 129 | expect { keyfinder }.to raise_error(JWT::VerificationError, error) 130 | end 131 | end 132 | 133 | context 'not given' do 134 | subject(:keyfinder) { described_class.new([root_certificate], nil).from(x5c_header) } 135 | 136 | it 'raises an error' do 137 | error = 'Certificate verification failed: unable to get certificate CRL. Certificate subject: /DC=org/DC=fake/CN=Fake.' 138 | expect { keyfinder }.to raise_error(JWT::VerificationError, error) 139 | end 140 | end 141 | end 142 | 143 | private 144 | 145 | def generate_key 146 | OpenSSL::PKey::RSA.new(2048) 147 | end 148 | 149 | def generate_root_cert(root_dn, root_key) 150 | cert = generate_cert(root_dn, root_key, 1) 151 | ef = OpenSSL::X509::ExtensionFactory.new 152 | cert.add_extension(ef.create_extension('basicConstraints', 'CA:TRUE', true)) 153 | cert.sign(root_key, 'sha256') 154 | cert 155 | end 156 | 157 | def generate_cert(subject, key, serial, issuer: nil, not_after: nil) 158 | cert = OpenSSL::X509::Certificate.new 159 | issuer ||= cert 160 | cert.version = 2 161 | cert.serial = serial 162 | cert.subject = subject 163 | cert.issuer = issuer.subject 164 | cert.public_key = key 165 | now = Time.now 166 | cert.not_before = now - 3600 167 | cert.not_after = not_after || (now + 3600) 168 | cert 169 | end 170 | 171 | def issue_crl(revocations, issuer:, issuer_key:, next_up: nil) 172 | crl = OpenSSL::X509::CRL.new 173 | crl.issuer = issuer.subject 174 | crl.version = 1 175 | now = Time.now 176 | crl.last_update = now - 3600 177 | crl.next_update = next_up || (now + 3600) 178 | 179 | revocations.each do |rserial, time, reason_code| 180 | revoked = build_revoked(rserial, time, reason_code) 181 | crl.add_revoked(revoked) 182 | end 183 | 184 | crlnum = OpenSSL::ASN1::Integer(1) 185 | crl.add_extension(OpenSSL::X509::Extension.new('crlNumber', crlnum)) 186 | 187 | crl.sign(issuer_key, 'sha256') 188 | crl 189 | end 190 | 191 | def build_revoked(rserial, time, reason_code) 192 | revoked = OpenSSL::X509::Revoked.new 193 | revoked.serial = rserial 194 | revoked.time = time 195 | enum = OpenSSL::ASN1::Enumerated(reason_code) 196 | ext = OpenSSL::X509::Extension.new('CRLReason', enum) 197 | revoked.add_extension(ext) 198 | revoked 199 | end 200 | end 201 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec' 4 | require 'simplecov' 5 | require 'jwt' 6 | 7 | require_relative 'spec_support/test_keys' 8 | require_relative 'spec_support/token' 9 | 10 | puts "OpenSSL::VERSION: #{OpenSSL::VERSION}" 11 | puts "OpenSSL::OPENSSL_VERSION: #{OpenSSL::OPENSSL_VERSION}" 12 | puts "OpenSSL::OPENSSL_LIBRARY_VERSION: #{OpenSSL::OPENSSL_LIBRARY_VERSION}\n\n" 13 | 14 | RSpec.configure do |config| 15 | config.expect_with :rspec do |c| 16 | c.syntax = :expect 17 | end 18 | config.include(SpecSupport::TestKeys) 19 | 20 | config.before(:example) do 21 | JWT.configuration.reset! 22 | JWT.configuration.deprecation_warnings = :warn 23 | end 24 | 25 | config.run_all_when_everything_filtered = true 26 | config.filter_run :focus 27 | config.order = 'random' 28 | end 29 | -------------------------------------------------------------------------------- /spec/spec_support/test_keys.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SpecSupport 4 | module TestKeys 5 | KEY_FIXTURE_PATH = File.join(__dir__, '..', 'fixtures', 'keys') 6 | 7 | def test_pkey(key) 8 | TestKeys.keys[key] ||= read_pkey(key) 9 | end 10 | 11 | def read_pkey(key) 12 | OpenSSL::PKey.read(File.read(File.join(KEY_FIXTURE_PATH, key))) 13 | end 14 | 15 | def self.keys 16 | @keys ||= {} 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/spec_support/token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SpecSupport 4 | Token = Struct.new(:payload, :header, keyword_init: true) 5 | end 6 | --------------------------------------------------------------------------------