diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml new file mode 100644 index 00000000..12e3a94f --- /dev/null +++ b/.github/workflows/deploy_docs.yml @@ -0,0 +1,29 @@ +--- +name: GitHub Pages + +on: + push: + branches: + - "main" + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ruby + bundler-cache: true + - name: Install yard + run: gem install yard + - name: Build docs + run: yard + - name: Deploy + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./doc diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fa2ed97..c97df46e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,27 +1,5 @@ # Changelog -## Upcoming breaking changes - -Notable changes in the upcoming **version 3.0**: - -- The indirect dependency to [rbnacl](https://github.com/RubyCrypto/rbnacl) will be removed: - - Support for the nonstandard SHA512256 algorithm will be removed. - - Support for Ed25519 will be moved to a [separate gem](https://github.com/anakinj/jwt-eddsa) for better dependency handling. - -- Base64 decoding will no longer fallback on the looser RFC 2045. - -- 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) and lead to the following deprecations: - - The `::JWT::ClaimsValidator` class will be removed in favor of the functionality provided by `::JWT::Claims`. - - The `::JWT::Claims::verify!` method will be removed in favor of `::JWT::Claims::verify_payload!`. - - The `::JWT::JWA.create` method will be removed. No recommended alternatives. - - The `::JWT::Verify` class will be removed in favor of the functionality provided by `::JWT::Claims`. - - Calling `::JWT::Claims::Numeric.new` with a payload will be removed in favor of `::JWT::Claims::verify_payload!(payload, :numeric)`. - - Calling `::JWT::Claims::Numeric.verify!` with a payload will be removed in favor of `::JWT::Claims::verify_payload!(payload, :numeric)`. - -- The internal algorithms were [restructured](https://github.com/jwt/ruby-jwt/pull/607) to support extensions from separate libraries. The changes lead to a few deprecations and new requirements: - - The `sign` and `verify` static methods on all the algorithms (`::JWT::JWA`) will be removed. - - Custom algorithms are expected to include the `JWT::JWA::SigningAlgorithm` module. - ## [v2.9.4](https://github.com/jwt/ruby-jwt/tree/v2.9.4) (NEXT) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.9.3...main) @@ -33,6 +11,8 @@ Notable changes in the upcoming **version 3.0**: **Fixes and enhancements:** +- Deprecation warnings for deprecated methods and classes [#629](https://github.com/jwt/ruby-jwt/pull/629) ([@anakinj](https://github.com/anakinj)) +- Improved documentation for public apis [#629](https://github.com/jwt/ruby-jwt/pull/629) ([@anakinj](https://github.com/anakinj)) - Your contribution here ## [v2.9.3](https://github.com/jwt/ruby-jwt/tree/v2.9.3) (2024-10-03) diff --git a/README.md b/README.md index 48c4568e..9a0f6816 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,30 @@ A ruby implementation of the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools If you have further questions related to development or usage, join us: [ruby-jwt google group](https://groups.google.com/forum/#!forum/ruby-jwt). -## Announcements -* Ruby 2.4 support was dropped in version 2.4.0 -* Ruby 1.9.3 support was dropped at December 31st, 2016. -* Version 1.5.3 yanked. See: [#132](https://github.com/jwt/ruby-jwt/issues/132) and [#133](https://github.com/jwt/ruby-jwt/issues/133) - See [CHANGELOG.md](CHANGELOG.md) for a complete set of changes. +## Upcoming breaking changes + +Notable changes in the upcoming **version 3.0**: + +- The indirect dependency to [rbnacl](https://github.com/RubyCrypto/rbnacl) will be removed: + - Support for the non-standard SHA512256 algorithm will be removed. + - Support for Ed25519 will be moved to a [separate gem](https://github.com/anakinj/jwt-eddsa) for better dependency handling. + +- Base64 decoding will no longer fallback on the looser RFC 2045. + +- 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) and lead to the following deprecations: + - The `::JWT::ClaimsValidator` class will be removed in favor of the functionality provided by `::JWT::Claims`. + - The `::JWT::Claims::verify!` method will be removed in favor of `::JWT::Claims::verify_payload!`. + - The `::JWT::JWA.create` method will be removed. + - The `::JWT::Verify` class will be removed in favor of the functionality provided by `::JWT::Claims`. + - Calling `::JWT::Claims::Numeric.new` with a payload will be removed in favor of `::JWT::Claims::verify_payload!(payload, :numeric)`. + - Calling `::JWT::Claims::Numeric.verify!` with a payload will be removed in favor of `::JWT::Claims::verify_payload!(payload, :numeric)`. + +- The internal algorithms were [restructured](https://github.com/jwt/ruby-jwt/pull/607) to support extensions from separate libraries. The changes lead to a few deprecations and new requirements: + - The `sign` and `verify` static methods on all the algorithms (`::JWT::JWA`) will be removed. + - Custom algorithms are expected to include the `JWT::JWA::SigningAlgorithm` module. + ## Sponsors |Logo|Message| @@ -26,20 +43,32 @@ See [CHANGELOG.md](CHANGELOG.md) for a complete set of changes. ## Installing ### Using Rubygems: + ```bash gem install jwt ``` ### Using Bundler: + Add the following to your Gemfile ``` gem 'jwt' ``` + And run `bundle install` +Finally require the gem in your application +```ruby +require 'jwt' +``` + ## Algorithms and Usage -The JWT spec supports NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms for cryptographic signing. Currently the jwt gem supports NONE, HMAC, RSASSA and ECDSA. If you are using cryptographic signing, you need to specify the algorithm in the options hash whenever you call JWT.decode to ensure that an attacker [cannot bypass the algorithm verification step](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/). **It is strongly recommended that you hard code the algorithm, as you may leave yourself vulnerable by dynamically picking the algorithm** +The jwt gem natively supports the NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms via the openssl library. The gem can be extended with additional or alternative implementations of the algorithms via extensions. + +Additionally the EdDSA algorithm is supported via a [separate gem](https://rubygems.org/gems/jwt-eddsa). + +For safe cryptographic signing, you need to specify the algorithm in the options hash whenever you call `JWT.decode` to ensure that an attacker [cannot bypass the algorithm verification step](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/). **It is strongly recommended that you hard code the algorithm, as you may leave yourself vulnerable by dynamically picking the algorithm** See: [ JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values for JWS](https://tools.ietf.org/html/rfc7518#section-3.1) @@ -65,18 +94,17 @@ The stricter base64 decoding when processing tokens can be done via the `strict_ * none - unsigned token ```ruby -require 'jwt' payload = { data: 'test' } # IMPORTANT: set nil as password parameter -token = JWT.encode payload, nil, 'none' +token = JWT.encode(payload, nil, 'none') # eyJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9. puts token # Set password to nil and validation to false otherwise this won't work -decoded_token = JWT.decode token, nil, false +decoded_token = JWT.decode(token, nil, false) # Array # [ @@ -96,12 +124,12 @@ puts decoded_token # The secret must be a string. With OpenSSL 3.0/openssl gem `<3.0.1`, JWT::DecodeError will be raised if it isn't provided. hmac_secret = 'my$ecretK3y' -token = JWT.encode payload, hmac_secret, 'HS256' +token = JWT.encode(payload, hmac_secret, 'HS256') # eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pNIWIL34Jo13LViZAJACzK6Yf0qnvT_BuwOxiMCPE-Y puts token -decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' } +decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' }) # Array # [ @@ -118,15 +146,15 @@ puts decoded_token * RS512 - RSA using SHA-512 hash algorithm ```ruby -rsa_private = OpenSSL::PKey::RSA.generate 2048 +rsa_private = OpenSSL::PKey::RSA.generate(2048) rsa_public = rsa_private.public_key -token = JWT.encode payload, rsa_private, 'RS256' +token = JWT.encode(payload, rsa_private, 'RS256') # eyJhbGciOiJSUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.GplO4w1spRgvEJQ3-FOtZr-uC8L45Jt7SN0J4woBnEXG_OZBSNcZjAJWpjadVYEe2ev3oUBFDYM1N_-0BTVeFGGYvMewu8E6aMjSZvOpf1cZBew-Vt4poSq7goG2YRI_zNPt3af2lkPqXD796IKC5URrEvcgF5xFQ-6h07XRDpSRx1ECrNsUOt7UM3l1IB4doY11GzwQA5sHDTmUZ0-kBT76ZMf12Srg_N3hZwphxBtudYtN5VGZn420sVrQMdPE_7Ni3EiWT88j7WCr1xrF60l8sZT3yKCVleG7D2BEXacTntB7GktBv4Xo8OKnpwpqTpIlC05dMowMkz3rEAAYbQ puts token -decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' } +decoded_token = JWT.decode(token, rsa_public, true, { algorithm: 'RS256' }) # Array # [ @@ -146,12 +174,12 @@ puts decoded_token ```ruby ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1') -token = JWT.encode payload, ecdsa_key, 'ES256' +token = JWT.encode(payload, ecdsa_key, 'ES256') # eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg puts token -decoded_token = JWT.decode token, ecdsa_key, true, { algorithm: 'ES256' } +decoded_token = JWT.decode(token, ecdsa_key, true, { algorithm: 'ES256' }) # Array # [ @@ -206,12 +234,12 @@ gem 'openssl', '~> 2.1' rsa_private = OpenSSL::PKey::RSA.generate 2048 rsa_public = rsa_private.public_key -token = JWT.encode payload, rsa_private, 'PS256' +token = JWT.encode(payload, rsa_private, 'PS256') # eyJhbGciOiJQUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.KEmqagMUHM-NcmXo6818ZazVTIAkn9qU9KQFT1c5Iq91n0KRpAI84jj4ZCdkysDlWokFs3Dmn4MhcXP03oJKLFgnoPL40_Wgg9iFr0jnIVvnMUp1kp2RFUbL0jqExGTRA3LdAhuvw6ZByGD1bkcWjDXygjQw-hxILrT1bENjdr0JhFd-cB0-ps5SB0mwhFNcUw-OM3Uu30B1-mlFaelUY8jHJYKwLTZPNxHzndt8RGXF8iZLp7dGb06HSCKMcVzhASGMH4ZdFystRe2hh31cwcvnl-Eo_D4cdwmpN3Abhk_8rkxawQJR3duh8HNKc4AyFPo7SabEaSu2gLnLfN3yfg puts token -decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'PS256' } +decoded_token = JWT.decode(token, rsa_public, true, { algorithm: 'PS256' }) # Array # [ @@ -293,24 +321,23 @@ Ruby-jwt gem supports custom [header fields](https://tools.ietf.org/html/rfc7519 To add custom header fields you need to pass `header_fields` parameter ```ruby -token = JWT.encode payload, key, algorithm='HS256', header_fields={} +token = JWT.encode(payload, key, algorithm='HS256', header_fields={}) ``` **Example:** ```ruby -require 'jwt' payload = { data: 'test' } # IMPORTANT: set nil as password parameter -token = JWT.encode payload, nil, 'none', { typ: 'JWT' } +token = JWT.encode(payload, nil, 'none', { typ: 'JWT' }) # eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjoidGVzdCJ9. puts token # Set password to nil and validation to false otherwise this won't work -decoded_token = JWT.decode token, nil, false +decoded_token = JWT.decode(token, nil, false) # Array # [ @@ -332,10 +359,10 @@ From [Oauth JSON Web Token 4.1.4. "exp" (Expiration Time) Claim](https://tools.i exp = Time.now.to_i + 4 * 3600 exp_payload = { data: 'data', exp: exp } -token = JWT.encode exp_payload, hmac_secret, 'HS256' +token = JWT.encode(exp_payload, hmac_secret, 'HS256') begin - decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' } + decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' }) rescue JWT::ExpiredSignature # Handle expired token, e.g. logout user or deny access end @@ -344,7 +371,7 @@ end The Expiration Claim verification can be disabled. ```ruby # Decode token without raising JWT::ExpiredSignature error -JWT.decode token, hmac_secret, true, { verify_expiration: false, algorithm: 'HS256' } +JWT.decode(token, hmac_secret, true, { verify_expiration: false, algorithm: 'HS256' }) ``` **Adding Leeway** @@ -356,11 +383,11 @@ leeway = 30 # seconds exp_payload = { data: 'data', exp: exp } # build expired token -token = JWT.encode exp_payload, hmac_secret, 'HS256' +token = JWT.encode(exp_payload, hmac_secret, 'HS256') begin # add leeway to ensure the token is still accepted - decoded_token = JWT.decode token, hmac_secret, true, { exp_leeway: leeway, algorithm: 'HS256' } + decoded_token = JWT.decode(token, hmac_secret, true, { exp_leeway: leeway, algorithm: 'HS256' }) rescue JWT::ExpiredSignature # Handle expired token, e.g. logout user or deny access end @@ -378,10 +405,10 @@ From [Oauth JSON Web Token 4.1.5. "nbf" (Not Before) Claim](https://tools.ietf.o nbf = Time.now.to_i - 3600 nbf_payload = { data: 'data', nbf: nbf } -token = JWT.encode nbf_payload, hmac_secret, 'HS256' +token = JWT.encode(nbf_payload, hmac_secret, 'HS256') begin - decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' } + decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' }) rescue JWT::ImmatureSignature # Handle invalid token, e.g. logout user or deny access end @@ -390,7 +417,7 @@ end The Not Before Claim verification can be disabled. ```ruby # Decode token without raising JWT::ImmatureSignature error -JWT.decode token, hmac_secret, true, { verify_not_before: false, algorithm: 'HS256' } +JWT.decode(token, hmac_secret, true, { verify_not_before: false, algorithm: 'HS256' }) ``` **Adding Leeway** @@ -402,11 +429,11 @@ leeway = 30 nbf_payload = { data: 'data', nbf: nbf } # build expired token -token = JWT.encode nbf_payload, hmac_secret, 'HS256' +token = JWT.encode(nbf_payload, hmac_secret, 'HS256') begin # add leeway to ensure the token is valid - decoded_token = JWT.decode token, hmac_secret, true, { nbf_leeway: leeway, algorithm: 'HS256' } + decoded_token = JWT.decode(token, hmac_secret, true, { nbf_leeway: leeway, algorithm: 'HS256' }) rescue JWT::ImmatureSignature # Handle invalid token, e.g. logout user or deny access end @@ -424,11 +451,11 @@ You can pass multiple allowed issuers as an Array, verification will pass if one iss = 'My Awesome Company Inc. or https://my.awesome.website/' iss_payload = { data: 'data', iss: iss } -token = JWT.encode iss_payload, hmac_secret, 'HS256' +token = JWT.encode(iss_payload, hmac_secret, 'HS256') begin # Add iss to the validation to check if the token has been manipulated - decoded_token = JWT.decode token, hmac_secret, true, { iss: iss, verify_iss: true, algorithm: 'HS256' } + decoded_token = JWT.decode(token, hmac_secret, true, { iss: iss, verify_iss: true, algorithm: 'HS256' }) rescue JWT::InvalidIssuerError # Handle invalid token, e.g. logout user or deny access end @@ -439,24 +466,24 @@ On supported ruby versions (>= 2.5) you can also delegate to methods, on older v to convert them to proc (using `to_proc`) ```ruby -JWT.decode token, hmac_secret, true, +JWT.decode(token, hmac_secret, true, iss: %r'https://my.awesome.website/', verify_iss: true, - algorithm: 'HS256' + algorithm: 'HS256') ``` ```ruby -JWT.decode token, hmac_secret, true, +JWT.decode(token, hmac_secret, true, iss: ->(issuer) { issuer.start_with?('My Awesome Company Inc') }, verify_iss: true, - algorithm: 'HS256' + algorithm: 'HS256') ``` ```ruby -JWT.decode token, hmac_secret, true, +JWT.decode(token, hmac_secret, true, iss: method(:valid_issuer?), verify_iss: true, - algorithm: 'HS256' + algorithm: 'HS256') # somewhere in the same class: def valid_issuer?(issuer) @@ -474,11 +501,11 @@ From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org aud = ['Young', 'Old'] aud_payload = { data: 'data', aud: aud } -token = JWT.encode aud_payload, hmac_secret, 'HS256' +token = JWT.encode(aud_payload, hmac_secret, 'HS256') begin # Add aud to the validation to check if the token has been manipulated - decoded_token = JWT.decode token, hmac_secret, true, { aud: aud, verify_aud: true, algorithm: 'HS256' } + decoded_token = JWT.decode(token, hmac_secret, true, { aud: aud, verify_aud: true, algorithm: 'HS256' }) rescue JWT::InvalidAudError # Handle invalid token, e.g. logout user or deny access puts 'Audience Error' @@ -497,15 +524,15 @@ jti_raw = [hmac_secret, iat].join(':').to_s jti = Digest::MD5.hexdigest(jti_raw) jti_payload = { data: 'data', iat: iat, jti: jti } -token = JWT.encode jti_payload, hmac_secret, 'HS256' +token = JWT.encode(jti_payload, hmac_secret, 'HS256') begin # If :verify_jti is true, validation will pass if a JTI is present - #decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: true, algorithm: 'HS256' } + #decoded_token = JWT.decode(token, hmac_secret, true, { verify_jti: true, algorithm: 'HS256' }) # Alternatively, pass a proc with your own code to check if the JTI has already been used - decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti| my_validation_method(jti) }, algorithm: 'HS256' } + decoded_token = JWT.decode(token, hmac_secret, true, { verify_jti: proc { |jti| my_validation_method(jti) }, algorithm: 'HS256' }) # or - decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti, payload| my_validation_method(jti, payload) }, algorithm: 'HS256' } + decoded_token = JWT.decode(token, hmac_secret, true, { verify_jti: proc { |jti, payload| my_validation_method(jti, payload) }, algorithm: 'HS256' }) rescue JWT::InvalidJtiError # Handle invalid token, e.g. logout user or deny access puts 'Error' @@ -524,11 +551,11 @@ From [Oauth JSON Web Token 4.1.6. "iat" (Issued At) Claim](https://tools.ietf.or iat = Time.now.to_i iat_payload = { data: 'data', iat: iat } -token = JWT.encode iat_payload, hmac_secret, 'HS256' +token = JWT.encode(iat_payload, hmac_secret, 'HS256') begin # Add iat to the validation to check if the token has been manipulated - decoded_token = JWT.decode token, hmac_secret, true, { verify_iat: true, algorithm: 'HS256' } + decoded_token = JWT.decode(token, hmac_secret, true, { verify_iat: true, algorithm: 'HS256' }) rescue JWT::InvalidIatError # Handle invalid token, e.g. logout user or deny access end @@ -544,11 +571,11 @@ From [Oauth JSON Web Token 4.1.2. "sub" (Subject) Claim](https://tools.ietf.org/ sub = 'Subject' sub_payload = { data: 'data', sub: sub } -token = JWT.encode sub_payload, hmac_secret, 'HS256' +token = JWT.encode(sub_payload, hmac_secret, 'HS256') begin # Add sub to the validation to check if the token has been manipulated - decoded_token = JWT.decode token, hmac_secret, true, { sub: sub, verify_sub: true, algorithm: 'HS256' } + decoded_token = JWT.decode(token, hmac_secret, true, { sub: sub, verify_sub: true, algorithm: 'HS256' }) rescue JWT::InvalidSubError # Handle invalid token, e.g. logout user or deny access end @@ -570,8 +597,6 @@ JWT::Claims.verify_payload!({"exp" => Time.now.to_i - 10}, exp: { leeway: 11}) JWT::Claims.verify_payload!({"exp" => Time.now.to_i + 10, "sub" => "subject"}, :exp, sub: "subject") ``` - - ### Finding a Key To dynamically find the key for verifying the JWT signature, pass a block to the decode block. The block receives headers and the original payload as parameters. It should return with the key to verify the signature that was used to sign the JWT. @@ -582,7 +607,7 @@ iss_payload = { data: 'data', iss: issuers.first } secrets = { issuers.first => hmac_secret, issuers.last => 'hmac_secret2' } -token = JWT.encode iss_payload, hmac_secret, 'HS256' +token = JWT.encode(iss_payload, hmac_secret, 'HS256') begin # Add iss to the validation to check if the token has been manipulated @@ -599,7 +624,7 @@ end You can specify claims that must be present for decoding to be successful. JWT::MissingRequiredClaim will be raised if any are missing ```ruby # Will raise a JWT::MissingRequiredClaim error if the 'exp' claim is absent -JWT.decode token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' } +JWT.decode(token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' }) ``` ### X.509 certificates in x5c header diff --git a/lib/jwt.rb b/lib/jwt.rb index 779ec83e..f8a93a88 100644 --- a/lib/jwt.rb +++ b/lib/jwt.rb @@ -25,6 +25,13 @@ module JWT module_function + # Encodes a payload into a JWT. + # + # @param payload [Hash] the payload to encode. + # @param key [String] the key used to sign the JWT. + # @param algorithm [String] the algorithm used to sign the JWT. + # @param header_fields [Hash] additional headers to include in the JWT. + # @return [String] the encoded JWT. def encode(payload, key, algorithm = 'HS256', header_fields = {}) Encode.new(payload: payload, key: key, @@ -32,6 +39,13 @@ def encode(payload, key, algorithm = 'HS256', header_fields = {}) headers: header_fields).segments end + # Decodes a JWT to extract the payload and header + # + # @param jwt [String] the JWT to decode. + # @param key [String] the key used to verify the JWT. + # @param verify [Boolean] whether to verify the JWT signature. + # @param options [Hash] additional options for decoding. + # @return [Array] the decoded payload and headers. def decode(jwt, key = nil, verify = true, options = {}, &keyfinder) # rubocop:disable Style/OptionalBooleanParameter Deprecations.context do Decode.new(jwt, key, verify, configuration.decode.to_h.merge(options), &keyfinder).decode_segments diff --git a/lib/jwt/base64.rb b/lib/jwt/base64.rb index 20f56b6c..14c9db28 100644 --- a/lib/jwt/base64.rb +++ b/lib/jwt/base64.rb @@ -4,15 +4,18 @@ module JWT # Base64 encoding and decoding + # @api private class Base64 class << self # Encode a string with URL-safe Base64 complying with RFC 4648 (not padded). + # @api private def url_encode(str) ::Base64.urlsafe_encode64(str, padding: false) end # Decode a string with URL-safe Base64 complying with RFC 4648. # Deprecated support for RFC 2045 remains for now. ("All line breaks or other characters not found in Table 1 must be ignored by decoding software") + # @api private def url_decode(str) ::Base64.urlsafe_decode64(str) rescue ArgumentError => e diff --git a/lib/jwt/claims.rb b/lib/jwt/claims.rb index 461263ca..dde6838a 100644 --- a/lib/jwt/claims.rb +++ b/lib/jwt/claims.rb @@ -35,6 +35,7 @@ module Claims class << self # @deprecated Use {verify_payload!} instead. Will be removed in the next major version of ruby-jwt. def verify!(payload, options) + Deprecations.warning('The ::JWT::Claims.verify! method is deprecated will be removed in the next major version of ruby-jwt') DecodeVerifier.verify!(payload, options) end diff --git a/lib/jwt/claims/audience.rb b/lib/jwt/claims/audience.rb index 5ca512c7..f828fc59 100644 --- a/lib/jwt/claims/audience.rb +++ b/lib/jwt/claims/audience.rb @@ -2,11 +2,21 @@ module JWT module Claims + # The Audience class is responsible for validating the audience claim ('aud') in a JWT token. class Audience + # Initializes a new Audience instance. + # + # @param expected_audience [String, Array] the expected audience(s) for the JWT token. def initialize(expected_audience:) @expected_audience = expected_audience end + # Verifies the audience claim ('aud') in the JWT token. + # + # @param context [Object] the context containing the JWT payload. + # @param _args [Hash] additional arguments (not used). + # @raise [JWT::InvalidAudError] if the audience claim is invalid. + # @return [nil] def verify!(context:, **_args) aud = context.payload['aud'] raise JWT::InvalidAudError, "Invalid audience. Expected #{expected_audience}, received #{aud || ''}" if ([*aud] & [*expected_audience]).empty? diff --git a/lib/jwt/claims/expiration.rb b/lib/jwt/claims/expiration.rb index 071885ad..0412dc4c 100644 --- a/lib/jwt/claims/expiration.rb +++ b/lib/jwt/claims/expiration.rb @@ -2,11 +2,21 @@ module JWT module Claims + # The Expiration class is responsible for validating the expiration claim ('exp') in a JWT token. class Expiration + # Initializes a new Expiration instance. + # + # @param leeway [Integer] the amount of leeway (in seconds) to allow when validating the expiration time. Default: 0. def initialize(leeway:) @leeway = leeway || 0 end + # Verifies the expiration claim ('exp') in the JWT token. + # + # @param context [Object] the context containing the JWT payload. + # @param _args [Hash] additional arguments (not used). + # @raise [JWT::ExpiredSignature] if the token has expired. + # @return [nil] def verify!(context:, **_args) return unless context.payload.is_a?(Hash) return unless context.payload.key?('exp') diff --git a/lib/jwt/claims/issued_at.rb b/lib/jwt/claims/issued_at.rb index 6aaf5108..0eb08446 100644 --- a/lib/jwt/claims/issued_at.rb +++ b/lib/jwt/claims/issued_at.rb @@ -2,7 +2,14 @@ module JWT module Claims + # The IssuedAt class is responsible for validating the issued at claim ('iat') in a JWT token. class IssuedAt + # Verifies the issued at claim ('iat') in the JWT token. + # + # @param context [Object] the context containing the JWT payload. + # @param _args [Hash] additional arguments (not used). + # @raise [JWT::InvalidIatError] if the issued at claim is invalid. + # @return [nil] def verify!(context:, **_args) return unless context.payload.is_a?(Hash) return unless context.payload.key?('iat') diff --git a/lib/jwt/claims/issuer.rb b/lib/jwt/claims/issuer.rb index f973e7f0..ca878375 100644 --- a/lib/jwt/claims/issuer.rb +++ b/lib/jwt/claims/issuer.rb @@ -2,11 +2,21 @@ module JWT module Claims + # The Issuer class is responsible for validating the issuer claim ('iss') in a JWT token. class Issuer + # Initializes a new Issuer instance. + # + # @param issuers [String, Symbol, Array] the expected issuer(s) for the JWT token. def initialize(issuers:) @issuers = Array(issuers).map { |item| item.is_a?(Symbol) ? item.to_s : item } end + # Verifies the issuer claim ('iss') in the JWT token. + # + # @param context [Object] the context containing the JWT payload. + # @param _args [Hash] additional arguments (not used). + # @raise [JWT::InvalidIssuerError] if the issuer claim is invalid. + # @return [nil] def verify!(context:, **_args) case (iss = context.payload['iss']) when *issuers diff --git a/lib/jwt/claims/jwt_id.rb b/lib/jwt/claims/jwt_id.rb index 9dc5b0d0..8d17fdcc 100644 --- a/lib/jwt/claims/jwt_id.rb +++ b/lib/jwt/claims/jwt_id.rb @@ -2,11 +2,21 @@ module JWT module Claims + # The JwtId class is responsible for validating the JWT ID claim ('jti') in a JWT token. class JwtId + # Initializes a new JwtId instance. + # + # @param validator [#call] an object responding to `call` to validate the JWT ID. def initialize(validator:) @validator = validator end + # Verifies the JWT ID claim ('jti') in the JWT token. + # + # @param context [Object] the context containing the JWT payload. + # @param _args [Hash] additional arguments (not used). + # @raise [JWT::InvalidJtiError] if the JWT ID claim is invalid or missing. + # @return [nil] def verify!(context:, **_args) jti = context.payload['jti'] if validator.respond_to?(:call) diff --git a/lib/jwt/claims/not_before.rb b/lib/jwt/claims/not_before.rb index e53f6de3..879ad031 100644 --- a/lib/jwt/claims/not_before.rb +++ b/lib/jwt/claims/not_before.rb @@ -2,11 +2,21 @@ module JWT module Claims + # The NotBefore class is responsible for validating the 'nbf' (Not Before) claim in a JWT token. class NotBefore + # Initializes a new NotBefore instance. + # + # @param leeway [Integer] the amount of leeway (in seconds) to allow when validating the 'nbf' claim. Defaults to 0. def initialize(leeway:) @leeway = leeway || 0 end + # Verifies the 'nbf' (Not Before) claim in the JWT token. + # + # @param context [Object] the context containing the JWT payload. + # @param _args [Hash] additional arguments (not used). + # @raise [JWT::ImmatureSignature] if the 'nbf' claim has not been reached. + # @return [nil] def verify!(context:, **_args) return unless context.payload.is_a?(Hash) return unless context.payload.key?('nbf') diff --git a/lib/jwt/claims/numeric.rb b/lib/jwt/claims/numeric.rb index 6550c44a..4a6f8bed 100644 --- a/lib/jwt/claims/numeric.rb +++ b/lib/jwt/claims/numeric.rb @@ -2,7 +2,11 @@ module JWT module Claims + # The Numeric class is responsible for validating numeric claims in a JWT token. + # The numeric claims are: exp, iat and nbf class Numeric + # The Compat class provides backward compatibility for numeric claim validation. + # @api private class Compat def initialize(payload) @payload = payload @@ -13,23 +17,41 @@ def verify! end end + # List of numeric claims that can be validated. NUMERIC_CLAIMS = %i[ exp iat nbf ].freeze + private_constant(:NUMERIC_CLAIMS) + + # @api private def self.new(*args) return super if args.empty? + Deprecations.warning('Calling ::JWT::Claims::Numeric.new with the payload will be removed in the next major version of ruby-jwt') Compat.new(*args) end + # Verifies the numeric claims in the JWT context. + # + # @param context [Object] the context containing the JWT payload. + # @raise [JWT::InvalidClaimError] if any numeric claim is invalid. + # @return [nil] def verify!(context:) validate_numeric_claims(context.payload) end + # Verifies the numeric claims in the JWT payload. + # + # @param payload [Hash] the JWT payload containing the claims. + # @param _args [Hash] additional arguments (not used). + # @raise [JWT::InvalidClaimError] if any numeric claim is invalid. + # @return [nil] + # @deprecated The ::JWT::Claims::Numeric.verify! method will be removed in the next major version of ruby-jwt def self.verify!(payload:, **_args) + Deprecations.warning('The ::JWT::Claims::Numeric.verify! method will be removed in the next major version of ruby-jwt.') JWT::Claims.verify_payload!(payload, :numeric) end diff --git a/lib/jwt/claims/required.rb b/lib/jwt/claims/required.rb index a360821e..e0f0e1d7 100644 --- a/lib/jwt/claims/required.rb +++ b/lib/jwt/claims/required.rb @@ -2,11 +2,21 @@ module JWT module Claims + # The Required class is responsible for validating that all required claims are present in a JWT token. class Required + # Initializes a new Required instance. + # + # @param required_claims [Array] the list of required claims. def initialize(required_claims:) @required_claims = required_claims end + # Verifies that all required claims are present in the JWT payload. + # + # @param context [Object] the context containing the JWT payload. + # @param _args [Hash] additional arguments (not used). + # @raise [JWT::MissingRequiredClaim] if any required claim is missing. + # @return [nil] def verify!(context:, **_args) required_claims.each do |required_claim| next if context.payload.is_a?(Hash) && context.payload.key?(required_claim) diff --git a/lib/jwt/claims/subject.rb b/lib/jwt/claims/subject.rb index dd1df517..18b26eeb 100644 --- a/lib/jwt/claims/subject.rb +++ b/lib/jwt/claims/subject.rb @@ -2,11 +2,21 @@ module JWT module Claims + # The Subject class is responsible for validating the subject claim ('sub') in a JWT token. class Subject + # Initializes a new Subject instance. + # + # @param expected_subject [String] the expected subject for the JWT token. def initialize(expected_subject:) @expected_subject = expected_subject.to_s end + # Verifies the subject claim ('sub') in the JWT token. + # + # @param context [Object] the context containing the JWT payload. + # @param _args [Hash] additional arguments (not used). + # @raise [JWT::InvalidSubError] if the subject claim is invalid. + # @return [nil] def verify!(context:, **_args) sub = context.payload['sub'] raise(JWT::InvalidSubError, "Invalid subject. Expected #{expected_subject}, received #{sub || ''}") unless sub.to_s == expected_subject diff --git a/lib/jwt/claims_validator.rb b/lib/jwt/claims_validator.rb index fee9d60f..24bd41ae 100644 --- a/lib/jwt/claims_validator.rb +++ b/lib/jwt/claims_validator.rb @@ -1,13 +1,15 @@ # frozen_string_literal: true -require_relative 'error' - module JWT + # @deprecated Use `Claims.verify_payload!` directly instead. class ClaimsValidator + # @deprecated Use `Claims.verify_payload!` directly instead. def initialize(payload) + Deprecations.warning('The ::JWT::ClaimsValidator class is deprecated and will be removed in the next major version of ruby-jwt') @payload = payload end + # @deprecated Use `Claims.verify_payload!` directly instead. def validate! Claims.verify_payload!(@payload, :numeric) true diff --git a/lib/jwt/configuration.rb b/lib/jwt/configuration.rb index de157901..cdd37a4a 100644 --- a/lib/jwt/configuration.rb +++ b/lib/jwt/configuration.rb @@ -3,11 +3,19 @@ require_relative 'configuration/container' module JWT + # The Configuration module provides methods to configure JWT settings. module Configuration + # Configures the JWT settings. + # + # @yield [config] Gives the current configuration to the block. + # @yieldparam config [JWT::Configuration::Container] the configuration container. def configure yield(configuration) end + # Returns the JWT configuration container. + # + # @return [JWT::Configuration::Container] the configuration container. def configuration @configuration ||= ::JWT::Configuration::Container.new end diff --git a/lib/jwt/configuration/container.rb b/lib/jwt/configuration/container.rb index dccaf237..33e97c8d 100644 --- a/lib/jwt/configuration/container.rb +++ b/lib/jwt/configuration/container.rb @@ -5,14 +5,28 @@ module JWT module Configuration + # The Container class holds the configuration settings for JWT. class Container + # @!attribute [rw] decode + # @return [DecodeConfiguration] the decode configuration. + # @!attribute [rw] jwk + # @return [JwkConfiguration] the JWK configuration. + # @!attribute [rw] strict_base64_decoding + # @return [Boolean] whether strict Base64 decoding is enabled. attr_accessor :decode, :jwk, :strict_base64_decoding + + # @!attribute [r] deprecation_warnings + # @return [Symbol] the deprecation warnings setting. attr_reader :deprecation_warnings + # Initializes a new Container instance and resets the configuration. def initialize reset! end + # Resets the configuration to default values. + # + # @return [void] def reset! @decode = DecodeConfiguration.new @jwk = JwkConfiguration.new @@ -22,6 +36,12 @@ def reset! end DEPRECATION_WARNINGS_VALUES = %i[once warn silent].freeze + private_constant(:DEPRECATION_WARNINGS_VALUES) + # Sets the deprecation warnings setting. + # + # @param value [Symbol] the deprecation warnings setting. Must be one of `:once`, `:warn`, or `:silent`. + # @raise [ArgumentError] if the value is not one of the supported values. + # @return [void] def deprecation_warnings=(value) raise ArgumentError, "Invalid deprecation_warnings value #{value}. Supported values: #{DEPRECATION_WARNINGS_VALUES}" unless DEPRECATION_WARNINGS_VALUES.include?(value) diff --git a/lib/jwt/configuration/decode_configuration.rb b/lib/jwt/configuration/decode_configuration.rb index 1c7dab1c..4acfd3eb 100644 --- a/lib/jwt/configuration/decode_configuration.rb +++ b/lib/jwt/configuration/decode_configuration.rb @@ -2,7 +2,29 @@ module JWT module Configuration + # The DecodeConfiguration class holds the configuration settings for decoding JWT tokens. class DecodeConfiguration + # @!attribute [rw] verify_expiration + # @return [Boolean] whether to verify the expiration claim. + # @!attribute [rw] verify_not_before + # @return [Boolean] whether to verify the not before claim. + # @!attribute [rw] verify_iss + # @return [Boolean] whether to verify the issuer claim. + # @!attribute [rw] verify_iat + # @return [Boolean] whether to verify the issued at claim. + # @!attribute [rw] verify_jti + # @return [Boolean] whether to verify the JWT ID claim. + # @!attribute [rw] verify_aud + # @return [Boolean] whether to verify the audience claim. + # @!attribute [rw] verify_sub + # @return [Boolean] whether to verify the subject claim. + # @!attribute [rw] leeway + # @return [Integer] the leeway in seconds for time-based claims. + # @!attribute [rw] algorithms + # @return [Array] the list of acceptable algorithms. + # @!attribute [rw] required_claims + # @return [Array] the list of required claims. + attr_accessor :verify_expiration, :verify_not_before, :verify_iss, @@ -14,6 +36,7 @@ class DecodeConfiguration :algorithms, :required_claims + # Initializes a new DecodeConfiguration instance with default settings. def initialize @verify_expiration = true @verify_not_before = true @@ -27,6 +50,7 @@ def initialize @required_claims = [] end + # @api private def to_h { verify_expiration: verify_expiration, diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index c05f7213..743b6b43 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -3,10 +3,17 @@ require 'json' require 'jwt/x5c_key_finder' -# JWT::Decode module module JWT - # Decoding logic for JWT + # The Decode class is responsible for decoding and verifying JWT tokens. class Decode + # Initializes a new Decode instance. + # + # @param jwt [String] the JWT to decode. + # @param key [String, Array] the key(s) to use for verification. + # @param verify [Boolean] whether to verify the token's signature. + # @param options [Hash] additional options for decoding and verification. + # @param keyfinder [Proc] an optional key finder block to dynamically find the key for verification. + # @raise [JWT::DecodeError] if decoding or verification fails. def initialize(jwt, key, verify, options, &keyfinder) raise JWT::DecodeError, 'Nil JSON web token' unless jwt @@ -17,6 +24,9 @@ def initialize(jwt, key, verify, options, &keyfinder) @keyfinder = keyfinder end + # Decodes the JWT token and verifies its segments if verification is enabled. + # + # @return [Array] an array containing the decoded payload and header. def decode_segments validate_segment_count! if @verify diff --git a/lib/jwt/encode.rb b/lib/jwt/encode.rb index c99be282..32164e09 100644 --- a/lib/jwt/encode.rb +++ b/lib/jwt/encode.rb @@ -3,13 +3,24 @@ require_relative 'jwa' module JWT + # The Encode class is responsible for encoding JWT tokens. class Encode + # Initializes a new Encode instance. + # + # @param options [Hash] the options for encoding the JWT token. + # @option options [Hash] :payload the payload of the JWT token. + # @option options [Hash] :headers the headers of the JWT token. + # @option options [String] :key the key used to sign the JWT token. + # @option options [String] :algorithm the algorithm used to sign the JWT token. def initialize(options) @token = Token.new(payload: options[:payload], header: options[:headers]) @key = options[:key] @algorithm = options[:algorithm] end + # Encodes the JWT token and returns its segments. + # + # @return [String] the encoded JWT token. def segments @token.verify_claims!(:numeric) @token.sign!(algorithm: @algorithm, key: @key) diff --git a/lib/jwt/error.rb b/lib/jwt/error.rb index a5d91651..d6ea9e91 100644 --- a/lib/jwt/error.rb +++ b/lib/jwt/error.rb @@ -1,23 +1,54 @@ # frozen_string_literal: true module JWT + # The EncodeError class is raised when there is an error encoding a JWT. class EncodeError < StandardError; end + + # The DecodeError class is raised when there is an error decoding a JWT. class DecodeError < StandardError; end + + # The RequiredDependencyError class is raised when a required dependency is missing. class RequiredDependencyError < StandardError; end + # The VerificationError class is raised when there is an error verifying a JWT. class VerificationError < DecodeError; end + + # The ExpiredSignature class is raised when the JWT signature has expired. class ExpiredSignature < DecodeError; end + + # The IncorrectAlgorithm class is raised when the JWT algorithm is incorrect. class IncorrectAlgorithm < DecodeError; end + + # The ImmatureSignature class is raised when the JWT signature is immature. class ImmatureSignature < DecodeError; end + + # The InvalidIssuerError class is raised when the JWT issuer is invalid. class InvalidIssuerError < DecodeError; end + + # The UnsupportedEcdsaCurve class is raised when the ECDSA curve is unsupported. class UnsupportedEcdsaCurve < IncorrectAlgorithm; end + + # The InvalidIatError class is raised when the JWT issued at (iat) claim is invalid. class InvalidIatError < DecodeError; end + + # The InvalidAudError class is raised when the JWT audience (aud) claim is invalid. class InvalidAudError < DecodeError; end + + # The InvalidSubError class is raised when the JWT subject (sub) claim is invalid. class InvalidSubError < DecodeError; end + + # The InvalidJtiError class is raised when the JWT ID (jti) claim is invalid. class InvalidJtiError < DecodeError; end + + # The InvalidPayload class is raised when the JWT payload is invalid. class InvalidPayload < DecodeError; end + + # The MissingRequiredClaim class is raised when a required claim is missing from the JWT. class MissingRequiredClaim < DecodeError; end + + # The Base64DecodeError class is raised when there is an error decoding a Base64-encoded string. class Base64DecodeError < DecodeError; end + # The JWKError class is raised when there is an error with the JSON Web Key (JWK). class JWKError < DecodeError; end end diff --git a/lib/jwt/jwa.rb b/lib/jwt/jwa.rb index 914c54a1..943fc96b 100644 --- a/lib/jwt/jwa.rb +++ b/lib/jwt/jwa.rb @@ -29,8 +29,10 @@ end module JWT + # The JWA module contains all supported algorithms. module JWA class << self + # @api private def resolve(algorithm) return find(algorithm) if algorithm.is_a?(String) || algorithm.is_a?(Symbol) @@ -48,7 +50,9 @@ def resolve_and_sort(algorithms:, preferred_algorithm:) algs.partition { |alg| alg.valid_alg?(preferred_algorithm) }.flatten end + # @deprecated The `::JWT::JWA.create` method is deprecated and will be removed in the next major version of ruby-jwt. def create(algorithm) + Deprecations.warning('The ::JWT::JWA.create method is deprecated and will be removed in the next major version of ruby-jwt.') resolve(algorithm) end end diff --git a/lib/jwt/jwa/compat.rb b/lib/jwt/jwa/compat.rb index 297ced69..fc80d162 100644 --- a/lib/jwt/jwa/compat.rb +++ b/lib/jwt/jwa/compat.rb @@ -2,7 +2,10 @@ module JWT module JWA + # Provides backwards compatibility for algorithms + # @api private module Compat + # @api private module ClassMethods def from_algorithm(algorithm) new(algorithm) diff --git a/lib/jwt/verify.rb b/lib/jwt/verify.rb index d29efa01..eebe0fbf 100644 --- a/lib/jwt/verify.rb +++ b/lib/jwt/verify.rb @@ -3,10 +3,12 @@ require_relative 'error' module JWT + # @deprecated This class is deprecated and will be removed in the next major version of ruby-jwt. class Verify DEFAULTS = { leeway: 0 }.freeze METHODS = %w[verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub verify_required_claims].freeze + private_constant(:DEFAULTS, :METHODS) class << self METHODS.each do |method_name| define_method(method_name) do |payload, options| @@ -14,13 +16,17 @@ class << self end end + # @deprecated This method is deprecated and will be removed in the next major version of ruby-jwt. def verify_claims(payload, options) + Deprecations.warning('The ::JWT::Verify.verify_claims method is deprecated and will be removed in the next major version of ruby-jwt') ::JWT::Claims.verify!(payload, options) true end end + # @deprecated This class is deprecated and will be removed in the next major version of ruby-jwt. def initialize(payload, options) + Deprecations.warning('The ::JWT::Verify class is deprecated and will be removed in the next major version of ruby-jwt') @payload = payload @options = DEFAULTS.merge(options) end diff --git a/lib/jwt/version.rb b/lib/jwt/version.rb index 6d0e3df5..71ebdff7 100644 --- a/lib/jwt/version.rb +++ b/lib/jwt/version.rb @@ -1,44 +1,63 @@ # frozen_string_literal: true -# Moments version builder module module JWT + # Returns the gem version of the JWT library. + # + # @return [Gem::Version] the gem version. def self.gem_version - Gem::Version.new VERSION::STRING + Gem::Version.new(VERSION::STRING) end - # Moments version builder module + # @api private module VERSION - # major version MAJOR = 2 - # minor version MINOR = 9 - # tiny version TINY = 4 - # alpha, beta, etc. tag PRE = nil - # Build version string STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') + + private_constant(:MAJOR, :MINOR, :TINY, :PRE) end + # Checks if the OpenSSL version is 3 or greater. + # + # @return [Boolean] true if OpenSSL version is 3 or greater, false otherwise. + # @api private def self.openssl_3? return false if OpenSSL::OPENSSL_VERSION.include?('LibreSSL') true if 3 * 0x10000000 <= OpenSSL::OPENSSL_VERSION_NUMBER end + # Checks if the RbNaCl library is defined. + # + # @return [Boolean] true if RbNaCl is defined, false otherwise. + # @api private def self.rbnacl? defined?(::RbNaCl) end + # Checks if the RbNaCl library version is 6.0.0 or greater. + # + # @return [Boolean] true if RbNaCl version is 6.0.0 or greater, false otherwise. + # @api private def self.rbnacl_6_or_greater? rbnacl? && ::Gem::Version.new(::RbNaCl::VERSION) >= ::Gem::Version.new('6.0.0') end + # Checks if there is an OpenSSL 3 HMAC empty key regression. + # + # @return [Boolean] true if there is an OpenSSL 3 HMAC empty key regression, false otherwise. + # @api private def self.openssl_3_hmac_empty_key_regression? openssl_3? && openssl_version <= ::Gem::Version.new('3.0.0') end + # Returns the OpenSSL version. + # + # @return [Gem::Version] the OpenSSL version. + # @api private def self.openssl_version @openssl_version ||= ::Gem::Version.new(OpenSSL::VERSION) end