├── .gitignore ├── .rspec ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── json_web_token.gemspec ├── lib ├── json_web_token.rb └── json_web_token │ ├── algorithm │ ├── common.rb │ ├── ecdsa.rb │ ├── hmac.rb │ ├── rsa.rb │ └── rsa_util.rb │ ├── format │ ├── asn1.rb │ └── base64_url.rb │ ├── jwa.rb │ ├── jws.rb │ ├── jwt.rb │ ├── util.rb │ └── version.rb └── spec ├── fixtures └── rsa │ ├── private_key.pem │ ├── private_key_weak.pem │ ├── public_key.pem │ └── public_key_alt.pem ├── json_web_token ├── algorithm │ ├── ecdsa_spec.rb │ ├── hmac_spec.rb │ └── rsa_spec.rb ├── format │ ├── asn1_spec.rb │ └── base64_url_spec.rb ├── jwa_spec.rb ├── jws_spec.rb ├── jwt_spec.rb └── util_spec.rb ├── json_web_token_spec.rb ├── spec_helper.rb └── support ├── ecdsa_key.rb └── plausible_jwt.rb /.gitignore: -------------------------------------------------------------------------------- 1 | # https://raw.githubusercontent.com/github/gitignore/master/Ruby.gitignore 2 | 3 | *.gem 4 | *.rbc 5 | /.config 6 | /coverage/ 7 | /InstalledFiles 8 | /pkg/ 9 | /spec/reports/ 10 | /test/tmp/ 11 | /test/version_tmp/ 12 | /tmp/ 13 | 14 | ## Documentation cache and generated files: 15 | /.yardoc/ 16 | /_yardoc/ 17 | /doc/ 18 | /rdoc/ 19 | 20 | ## Environment normalisation: 21 | /.bundle/ 22 | /vendor/bundle 23 | /lib/bundler/man/ 24 | 25 | # for a library or gem, you might want to ignore these files since the code is 26 | # intended to run in multiple environments; otherwise, check them in: 27 | Gemfile.lock 28 | .ruby-version 29 | .ruby-gemset 30 | 31 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 32 | .rvmrc 33 | 34 | # custom 35 | /spec/examples.txt 36 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | rvm: 4 | - 2.2.7 5 | - 2.3.4 6 | - 2.4.1 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ### v0.3.5 (2017-06-17) 4 | 5 | * Bug fixes 6 | * Replace ECDSA curve secp256k1 in spec with prime256v1 (aka secp256r1) 7 | 8 | * Enhancements 9 | * Remove travis CI for ruby < v2.2 10 | * Update gem dependency versions 11 | 12 | ### v0.3.4 (2017-03-02) 13 | 14 | * Enhancements 15 | * Alias `JWT` for `JsonWebToken` can be turned off with `RUBY_GEM_JSON_WEB_TOKEN_SKIP_ALIAS` environment variable 16 | 17 | ### v0.3.3 (2017-01-16) 18 | 19 | * Bug fixes 20 | * Remove invalid RSA .validate_message_size 21 | 22 | ### v0.3.2 (2016-10-09) 23 | 24 | * Enhancements 25 | * Modernized the dev environment 26 | * Added a `Support for JWT Registered Claims` section to the README, along with a link to the companion `jwt_claims` gem 27 | * Fixed the README examples by using working tokens 28 | 29 | ### v0.3.1 (2015-08-25) 30 | 31 | * Bug fixes 32 | * README escaping removed 33 | 34 | ### v0.3.0 (2015-08-25) 35 | 36 | * Backward incompatible changes 37 | * JsonWebToken, Jwt, and Jws #verify return values 38 | 39 | ### v0.2.2 (2015-08-06) 40 | 41 | * Enhancements 42 | * RsaUtil to read keys from pem files 43 | 44 | ### v0.2.1 (2015-08-03) 45 | 46 | * Enhancements 47 | * Rsa#validate\_message\_size 48 | 49 | ### v0.2.0 (2015-08-02) 50 | 51 | * Backward incompatible changes 52 | * Top level API now #sign and #verify 53 | 54 | ### v0.1.2 (2015-08-02) 55 | 56 | * Enhancements 57 | * Jws#verify returns false (rather than 'Invalid') unless the signature is verified 58 | 59 | ### v0.1.1 (2015-07-13) 60 | 61 | * Bug fixes 62 | * #symbolize_keys spec failing on < ruby-2.2.0 63 | 64 | ### v0.1.0 (2015-07-12) 65 | 66 | * Enhancements 67 | * support ECDSA algorithm 68 | 69 | ### v0.0.2 (2015-07-11) 70 | 71 | * Enhancements 72 | * support RSASSA-PKCS-v1_5 algorithm 73 | 74 | ### v0.0.1 (2015-07-09) 75 | 76 | * Initial 77 | * support HMAC algorithm 78 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in json_web_token.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Gary Fleshman 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Web Token [![travis][ci_img]][travis] [![yard docs][yd_img]][yard_docs] [![code climate][cc_img]][code_climate] 2 | 3 | ## A JSON Web Token (JWT) implementation for Ruby 4 | 5 | ### Description 6 | A Ruby implementation of the JSON Web Token standard [RFC 7519][rfc7519] 7 | 8 | ## Installation 9 | gem install json_web_token 10 | 11 | ### Philosophy & Design Goals 12 | * Minimal API surface area 13 | * Clear separation and conformance to underlying standards 14 | - JSON Web Signature (JWS) Standards Track [RFC 7515][rfc7515] 15 | - JSON Web Algorithms (JWA) Standards Track [RFC 7518][rfc7518] 16 | * Thorough test coverage 17 | * Modularity for comprehension and extensibility 18 | * Fail fast and hard, with maximally strict validation 19 | - Inspired by [The Harmful Consequences of Postel's Maxim][thomson-postel] 20 | * Implement only the REQUIRED elements of the JWT standard (initially) 21 | 22 | ### Intended Audience 23 | Token authentication of API requests to Rails via these prominent gems: 24 | 25 | - [Devise][devise] 26 | - [Doorkeeper][doorkeeper] 27 | - [OAuth2][oauth2] 28 | 29 | Secure Cross-Origin Resource Sharing ([CORS][cors]) using the [rack-cors][rack-cors] gem 30 | 31 | ### Support for JWT Registered Claims 32 | 33 | Support for the standard registered claims documented 34 | in [RFC 7519][rfc7519] can be found in the companion gem [jwt_claims](https://github.com/garyf/jwt_claims). 35 | 36 | `jwt_claims` is a wrapper around `json_web_token` and provides support 37 | for the full set of registered claims. 38 | 39 | [https://github.com/garyf/jwt_claims](https://github.com/garyf/jwt_claims) 40 | 41 | ## Usage 42 | 43 | ### JsonWebToken.sign(claims, options) 44 | 45 | Returns a JSON Web Token string 46 | 47 | `claims` (required) string or hash 48 | 49 | `options` (required) hash 50 | 51 | * **alg** (optional, default: `HS256`) 52 | * **key** (required unless alg is 'none') 53 | 54 | Example 55 | 56 | ```ruby 57 | require 'json_web_token' 58 | 59 | # Sign with the default algorithm, HMAC SHA256 60 | jwt = JsonWebToken.sign({foo: 'bar'}, key: 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C') 61 | #=> "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIifQ.vpaYTGkypBmxDi3KZYcvpqLx9xqhRD-DSXONGrUbf5Q" 62 | 63 | # Sign with RSA SHA256 algorithm 64 | opts = { 65 | alg: 'RSA256', 66 | key: < RSA private key > 67 | } 68 | 69 | jwt = JsonWebToken.sign({foo: 'bar'}, opts) 70 | 71 | # Create an unsecured token (algorithm is 'none') 72 | jwt = JsonWebToken.sign({foo: 'bar'}, alg: 'none') 73 | 74 | ``` 75 | 76 | ### JsonWebToken.verify(jwt, options) 77 | 78 | Returns a hash: 79 | * \{ok: < JWT claims set >\}, if the Message Authentication Code (MAC), or signature, is verified 80 | * \{error: 'invalid'\}, otherwise 81 | 82 | `jwt` (required) is a JSON web token string 83 | 84 | `options` (required) hash 85 | 86 | * **alg** (optional, default: `HS256`) 87 | * **key** (required unless alg is 'none') 88 | 89 | Example 90 | 91 | ```ruby 92 | require 'json_web_token' 93 | 94 | jwt = JsonWebToken.sign({foo: 'bar'}, key: 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C') 95 | #=> "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIifQ.vpaYTGkypBmxDi3KZYcvpqLx9xqhRD-DSXONGrUbf5Q" 96 | 97 | # Verify with default algorithm, HMAC SHA256 98 | # Returns a hash of `{:ok, verified_claims}` 99 | JsonWebToken.verify(jwt, key: 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C') 100 | #=> {:ok=>{:foo=>"bar"}} 101 | 102 | # verify with RSA SHA256 algorithm 103 | opts = { 104 | alg: 'RSA256', 105 | key: < RSA public key > 106 | } 107 | 108 | {ok: claims} = JsonWebToken.verify(jwt, opts) 109 | 110 | # Unsecured token (algorithm is 'none') 111 | jwt = JsonWebToken.sign({foo: 'bar'}, alg: 'none') 112 | #=> "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJmb28iOiJiYXIifQ." 113 | 114 | JsonWebToken.verify(jwt, alg: 'none') 115 | #=> {:ok=>{:foo=>"bar"}} 116 | ``` 117 | 118 | ### Supported encryption algorithms 119 | 120 | alg Param Value | Digital Signature or MAC Algorithm 121 | ------|------ 122 | HS256 | HMAC using SHA-256 per [RFC 2104][rfc2104] 123 | HS384 | HMAC using SHA-384 124 | HS512 | HMAC using SHA-512 125 | RS256 | RSASSA-PKCS-v1_5 using SHA-256 per [RFC3447][rfc3447] 126 | RS384 | RSASSA-PKCS-v1_5 using SHA-384 127 | RS512 | RSASSA-PKCS-v1_5 using SHA-512 128 | ES256 | ECDSA using P-256 and SHA-256 per [DSS][dss] 129 | ES384 | ECDSA using P-384 and SHA-384 130 | ES512 | ECDSA using P-521 and SHA-512 131 | none | No digital signature or MAC performed (unsecured) 132 | 133 | ### Supported Ruby Versions 134 | Ruby 2.2 and up 135 | 136 | ### Limitations 137 | Future implementation may include these features: 138 | 139 | - processing of OPTIONAL JWT registered claim names (e.g. 'exp') 140 | - representation of a JWT as a JSON Web Encryption (JWE) [RFC 7516][rfc7516] 141 | - OPTIONAL nested JWTs 142 | 143 | [rfc2104]: http://tools.ietf.org/html/rfc2104 144 | [rfc3447]: http://tools.ietf.org/html/rfc3447 145 | [rfc7515]: http://tools.ietf.org/html/rfc7515 146 | [rfc7516]: http://tools.ietf.org/html/rfc7516 147 | [rfc7518]: http://tools.ietf.org/html/rfc7518 148 | [rfc7519]: http://tools.ietf.org/html/rfc7519 149 | [dss]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf 150 | 151 | [thomson-postel]: https://tools.ietf.org/html/draft-thomson-postel-was-wrong-00 152 | [cors]: http://www.w3.org/TR/cors/ 153 | [devise]: https://github.com/plataformatec/devise 154 | [doorkeeper]: https://github.com/doorkeeper-gem/doorkeeper 155 | [oauth2]: https://github.com/intridea/oauth2 156 | [rack-cors]: https://github.com/cyu/rack-cors 157 | 158 | [travis]: https://travis-ci.org/garyf/json_web_token 159 | [ci_img]: https://travis-ci.org/garyf/json_web_token.svg?branch=master 160 | [yard_docs]: http://www.rubydoc.info/github/garyf/json_web_token 161 | [yd_img]: http://img.shields.io/badge/yard-docs-blue.svg 162 | [code_climate]: https://codeclimate.com/github/garyf/json_web_token 163 | [cc_img]: https://codeclimate.com/github/garyf/json_web_token/badges/gpa.svg 164 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | require 'yard' 4 | require 'wwtd/tasks' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | YARD::Rake::YardocTask.new 9 | 10 | task default: :spec 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'json_web_token' 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | require 'pry' 10 | Pry.start 11 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /json_web_token.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'json_web_token/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'json_web_token' 8 | s.version = JsonWebToken::VERSION 9 | s.authors = ['Gary Fleshman'] 10 | s.email = ['gfleshman@newforge-tech.com'] 11 | 12 | s.summary = 'JSON Web Token (JWT) for Ruby' 13 | s.description = 'Ruby implementation of the JSON Web Token (JWT) standard, RFC 7519' 14 | s.homepage = 'https://github.com/garyf/json_web_token' 15 | s.license = 'MIT' 16 | 17 | s.files = `git ls-files -z`.split("\x0").reject do |f| 18 | f.match(%r{^(test|spec|features)/}) 19 | end 20 | 21 | s.require_paths = ['lib'] 22 | 23 | s.platform = Gem::Platform::RUBY 24 | s.required_ruby_version = '>= 2.2.0' 25 | 26 | s.add_runtime_dependency 'json', '~> 2.1' 27 | 28 | s.add_development_dependency 'bundler', '~> 1.15' 29 | s.add_development_dependency 'rake', '~> 12.0' 30 | s.add_development_dependency 'rspec', '~> 3.6' 31 | s.add_development_dependency 'pry-byebug', '~> 3.4' 32 | s.add_development_dependency 'simplecov', '~> 0.14' 33 | s.add_development_dependency 'yard', '~> 0.9' 34 | s.add_development_dependency 'wwtd', '~> 1.3' 35 | end 36 | -------------------------------------------------------------------------------- /lib/json_web_token.rb: -------------------------------------------------------------------------------- 1 | require 'json_web_token/jwt' 2 | 3 | # Top level interface, or API, to sign and verify a JSON Web Token (JWT) 4 | # @see http://tools.ietf.org/html/rfc7519 5 | module JsonWebToken 6 | 7 | module_function 8 | 9 | # @param claims [Hash] a collection of name/value pairs asserting information about a subject 10 | # @param options [Hash] specify the desired signing algorithm and signing key 11 | # @return [String] a JSON Web Token, representing digitally signed claims 12 | # @example 13 | # claims = {iss: 'joe', exp: 1300819380, :'http://example.com/is_root' => true} 14 | # options = {alg: 'HS256', key: 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C'} 15 | # JsonWebToken.sign(claims, options) 16 | # # => 'eyJhbGciOiJIUzI1NiJ9.cGF5bG9hZA.uVTaOdyzp_f4mT_hfzU8LnCzdmlVC4t2itHDEYUZym4' 17 | def sign(claims, options) 18 | Jwt.sign(claims, options) 19 | end 20 | 21 | # @param jwt [String] a JSON Web Token 22 | # @param options [Hash] specify the desired verifying algorithm and verifying key 23 | # @return [Hash] +{ok: < the jwt claims set hash >}+ if the jwt verifies, 24 | # or +{error: 'Invalid'}+ otherwise 25 | # @example 26 | # jwt = 'eyJhbGciOiJIUzI1NiJ9.cGF5bG9hZA.uVTaOdyzp_f4mT_hfzU8LnCzdmlVC4t2itHDEYUZym4' 27 | # options = {alg: 'HS256', key: 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C'} 28 | # JsonWebToken.verify(jwt, options) 29 | # # => {ok: {iss: 'joe', exp: 1300819380, :'http://example.com/is_root' => true}} 30 | def verify(jwt, options) 31 | Jwt.verify(jwt, options) 32 | end 33 | end 34 | 35 | # alias 36 | JWT = JsonWebToken unless ENV['RUBY_GEM_JSON_WEB_TOKEN_SKIP_ALIAS'] 37 | -------------------------------------------------------------------------------- /lib/json_web_token/algorithm/common.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | 3 | module JsonWebToken 4 | module Algorithm 5 | module Common 6 | 7 | SHA_BITS = [ 8 | '256', 9 | '384', 10 | '512' 11 | ] 12 | 13 | def validate_key(sha_bits, key) 14 | validate_sha_bits(sha_bits) 15 | validate_key_size(sha_bits, key) 16 | end 17 | 18 | def validate_sha_bits(sha_bits) 19 | fail('Invalid sha_bits') unless SHA_BITS.include?(sha_bits) 20 | end 21 | 22 | def digest_new(sha_bits) 23 | OpenSSL::Digest.new("sha#{sha_bits}") 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/json_web_token/algorithm/ecdsa.rb: -------------------------------------------------------------------------------- 1 | require 'json_web_token/algorithm/common' 2 | require 'json_web_token/format/asn1' 3 | 4 | module JsonWebToken 5 | module Algorithm 6 | # Sign or verify a JSON Web Signature (JWS) structure using ECDSA 7 | # @see http://tools.ietf.org/html/rfc7518#section-3.4 8 | module Ecdsa 9 | 10 | extend JsonWebToken::Algorithm::Common 11 | extend JsonWebToken::Format::Asn1 12 | 13 | MAC_BYTE_COUNT = { 14 | '256' => 64, # prime256v1 aka: secp256r1 15 | '384' => 96, # secp384r1 16 | '512' => 132 # secp521r1 note difference (not 128) due to using 521-bit integers 17 | } 18 | 19 | module_function 20 | 21 | # @param sha_bits [String] desired security level in bits of the signature scheme 22 | # @param private_key [OpenSSL::PKey::EC] key used to sign a digital signature, or mac 23 | # @param signing_input [String] input payload for a mac computation 24 | # @return [BinaryString] a digital signature, or mac 25 | # @example 26 | # Ecdsa.sign('256', private_key, 'signing_input').bytes 27 | # # => [90, 34, 44, 252, 147, 130, 167, 173, 86, 191, 247, 93, 94, 12, 200, 30, 173, 115, 248, 89, 246, 222, 4, 213, 119, 74, 70, 20, 231, 194, 104, 103] 28 | def sign(sha_bits, private_key, signing_input) 29 | validate_key(sha_bits, private_key) 30 | der = private_key.dsa_sign_asn1(ssl_digest_hash sha_bits, signing_input) 31 | der_to_signature(der, sha_bits) 32 | end 33 | 34 | # @param mac [BinaryString] a digital signature, or mac 35 | # @param sha_bits [String] desired security level in bits of the signature scheme 36 | # @param public_key [OpenSSL::PKey::EC] key used to verify a digital signature, or mac 37 | # @param signing_input [String] input payload for a mac computation 38 | # @return [Boolean] a predicate to verify the signing_input for a given +mac+ 39 | # @example 40 | # Ecdsa.verify?(< binary_string >, '256', < public_key >, 'signing_input') 41 | # # => true 42 | def verify?(mac, sha_bits, public_key, signing_input) 43 | validate_key(sha_bits, public_key) 44 | validate_signature_size(mac, sha_bits) 45 | der = signature_to_der(mac, sha_bits) 46 | public_key.dsa_verify_asn1(ssl_digest_hash(sha_bits, signing_input), der) 47 | end 48 | 49 | def validate_key_size(_sha_bits, _key); end 50 | 51 | def ssl_digest_hash(sha_bits, signing_input) 52 | digest_new(sha_bits).digest(signing_input) 53 | end 54 | 55 | def validate_signature_size(mac, sha_bits) 56 | fail('Invalid signature') unless mac && mac.bytesize == MAC_BYTE_COUNT[sha_bits] 57 | end 58 | 59 | private_class_method :validate_key_size, 60 | :ssl_digest_hash, 61 | :validate_signature_size 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/json_web_token/algorithm/hmac.rb: -------------------------------------------------------------------------------- 1 | require 'json_web_token/algorithm/common' 2 | require 'json_web_token/util' 3 | 4 | module JsonWebToken 5 | module Algorithm 6 | # Sign or verify a JSON Web Signature (JWS) structure using HMAC with SHA-2 algorithms 7 | # @see http://tools.ietf.org/html/rfc7518#section-3.2 8 | module Hmac 9 | 10 | extend JsonWebToken::Algorithm::Common 11 | 12 | module_function 13 | 14 | # @param sha_bits [String] size of the hash output 15 | # @param shared_key [String] secret key used to sign and verify a digital signature, or mac 16 | # @param signing_input [String] input payload for a mac computation 17 | # @return [BinaryString] a digital signature, or mac 18 | # @example 19 | # shared_key = "gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C" 20 | # Hmac.sign('256', shared_key, 'signing_input').bytes 21 | # # => [90, 34, 44, 252, 147, 130, 167, 173, 86, 191, 247, 93, 94, 12, 200, 30, 173, 115, 248, 89, 246, 222, 4, 213, 119, 74, 70, 20, 231, 194, 104, 103] 22 | def sign(sha_bits, shared_key, signing_input) 23 | validate_key(sha_bits, shared_key) 24 | OpenSSL::HMAC.digest(digest_new(sha_bits), shared_key, signing_input) 25 | end 26 | 27 | # @param mac [BinaryString] a digital signature, or mac 28 | # @param (see #sign) 29 | # @return [Boolean] a predicate to verify the signing_input by comparing a given +mac+ 30 | # to the +mac+ for a newly signed message; comparison done in a constant-time manner 31 | # to thwart timing attacks 32 | # @example 33 | # shared_key = "gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C" 34 | # Hmac.verify?(< binary_string >, '256', shared_key, 'signing_input') 35 | # # => true 36 | def verify?(mac, sha_bits, shared_key, signing_input) 37 | Util.constant_time_compare?(mac, sign(sha_bits, shared_key, signing_input)) 38 | end 39 | 40 | def validate_key_size(sha_bits, key) 41 | fail('Invalid shared key') if weak_key?(sha_bits, key) 42 | end 43 | 44 | def weak_key?(sha_bits, key) 45 | !key || key.bytesize * 8 < sha_bits.to_i 46 | end 47 | 48 | private_class_method :validate_key_size, 49 | :weak_key? 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/json_web_token/algorithm/rsa.rb: -------------------------------------------------------------------------------- 1 | require 'json_web_token/algorithm/common' 2 | 3 | module JsonWebToken 4 | module Algorithm 5 | # Sign or verify a JSON Web Signature (JWS) structure using RSASSA-PKCS-v1_5 6 | # @see http://tools.ietf.org/html/rfc7518#section-3.3 7 | module Rsa 8 | extend JsonWebToken::Algorithm::Common 9 | 10 | KEY_BITS_MIN = 2048 11 | 12 | module_function 13 | 14 | # @param sha_bits [String] desired security level in bits of the signature scheme 15 | # @param private_key [OpenSSL::PKey::RSA] key used to sign a digital signature, or mac 16 | # @param signing_input [String] input payload for a mac computation 17 | # @return [BinaryString] a digital signature, or mac 18 | # @example 19 | # Rsa.sign('256', < private_key >, 'signing_input').bytes.length 20 | # # => 256 21 | def sign(sha_bits, private_key, signing_input) 22 | validate_key(sha_bits, private_key) 23 | private_key.sign(digest_new(sha_bits), signing_input) 24 | end 25 | 26 | # @param mac [BinaryString] a digital signature, or mac 27 | # @param sha_bits [String] desired security level in bits of the signature scheme 28 | # @param public_key [OpenSSL::PKey::RSA] key used to verify a digital signature, or mac 29 | # @param signing_input [String] input payload for a mac computation 30 | # @return [Boolean] a predicate to verify the signing_input for a given +mac+ 31 | # @example 32 | # Rsa.verify?(< binary_string >, '256', < public_key >, 'signing_input') 33 | # # => true 34 | def verify?(mac, sha_bits, public_key, signing_input) 35 | validate_key(sha_bits, public_key) 36 | public_key.verify(digest_new(sha_bits), mac, signing_input) 37 | end 38 | 39 | def validate_key_size(_sha_bits, key) 40 | fail('Invalid key: RSA modulus too small') if weak_key?(key) 41 | end 42 | 43 | # https://github.com/ruby/openssl/issues/5 44 | def weak_key?(key) 45 | !key || key.n.num_bits < KEY_BITS_MIN 46 | end 47 | 48 | private_class_method :validate_key_size, 49 | :weak_key? 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/json_web_token/algorithm/rsa_util.rb: -------------------------------------------------------------------------------- 1 | module JsonWebToken 2 | module Algorithm 3 | # Load encryption keys 4 | module RsaUtil 5 | 6 | module_function 7 | 8 | # Load an RSA private key from a pem file 9 | def private_key(path_to_keys, filename = 'private_key.pem') 10 | decoded_key(path_to_keys, filename) 11 | end 12 | 13 | # Load an RSA public key from a pem file 14 | def public_key(path_to_keys, filename = 'public_key.pem') 15 | decoded_key(path_to_keys, filename) 16 | end 17 | 18 | def decoded_key(path_to_keys, filename) 19 | OpenSSL::PKey::RSA.new(pem_read(path_to_keys, filename)) 20 | end 21 | 22 | def pem_read(path_to_keys, filename) 23 | File.read(File.join(path_to_keys, filename)) 24 | end 25 | 26 | private_class_method :decoded_key, 27 | :pem_read 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/json_web_token/format/asn1.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | 3 | module JsonWebToken 4 | module Format 5 | # ASN1 data structures are usually encoded using the Distinguished Encoding Rules (DER). 6 | # The ASN1 module provides the necessary classes that allow generation of ASN1 data 7 | # structures and the methods to encode them using a DER encoding. The decode method allows 8 | # parsing arbitrary DER-encoded data to a Ruby object that can then be modified and 9 | # re-encoded at will. 10 | # @see http://docs.ruby-lang.org/en/2.1.0/OpenSSL/ASN1.html 11 | module Asn1 12 | 13 | KEY_BITS = { 14 | '256' => 256, 15 | '384' => 384, 16 | '512' => 521 # note difference 17 | } 18 | 19 | module_function 20 | 21 | def der_to_signature(der, sha_bits) 22 | signature_pair = OpenSSL::ASN1.decode(der).value 23 | width = per_part_byte_count(sha_bits) 24 | signature_pair.map { |part| part.value.to_s(2).rjust(width, "\x00") }.join 25 | end 26 | 27 | def signature_to_der(signature, sha_bits) 28 | hsh = destructured_sig(signature, sha_bits) 29 | asn1_seq = OpenSSL::ASN1::Sequence.new([ 30 | asn1_int(hsh[:r]), 31 | asn1_int(hsh[:s]) 32 | ]) 33 | asn1_seq.to_der 34 | end 35 | 36 | def per_part_byte_count(sha_bits) 37 | bits = KEY_BITS[sha_bits] 38 | bits ? (bits + 7) / 8 : fail('Invalid sha_bits') 39 | end 40 | 41 | def destructured_sig(signature, sha_bits) 42 | n = per_part_byte_count(sha_bits) 43 | fail('Invalid signature length') unless signature.length == n * 2 44 | { 45 | r: signature[0, n], 46 | s: signature[n, n] 47 | } 48 | end 49 | 50 | def asn1_int(int) 51 | OpenSSL::ASN1::Integer.new(OpenSSL::BN.new int, 2) 52 | end 53 | 54 | private_class_method :per_part_byte_count, 55 | :destructured_sig, 56 | :asn1_int 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/json_web_token/format/base64_url.rb: -------------------------------------------------------------------------------- 1 | require 'base64' 2 | 3 | module JsonWebToken 4 | module Format 5 | # Provide base64url encoding and decoding functions without padding, based upon standard 6 | # base64 encoding and decoding functions that do use padding 7 | # @see http://tools.ietf.org/html/rfc7515#appendix-C 8 | module Base64Url 9 | module_function 10 | 11 | # @param str [String] 12 | # @return [String] a urlsafe_encode64 string with all trailing '=' padding removed 13 | # @example 14 | # Base64Url.encode('foo') 15 | # # => 'Zm9v' 16 | def encode(str) 17 | base64_padding_removed(Base64.urlsafe_encode64(str)) 18 | end 19 | 20 | # @param str [String] encoded as url_encode64 21 | # @return [String] with trailing '=' padding added before decoding 22 | # @example 23 | # Base64Url.decode("YmFy") 24 | # # => 'bar' 25 | def decode(str) 26 | Base64.urlsafe_decode64(base64_padding_added(str)) 27 | end 28 | 29 | def base64_padding_removed(encoded) 30 | encoded.gsub(/[=]/, '') 31 | end 32 | 33 | def base64_padding_added(str) 34 | mod = str.length % 4 35 | return str if mod == 0 36 | fail('Invalid base64 string') if mod == 1 37 | "#{str}#{'=' * (4 - mod)}" 38 | end 39 | 40 | private_class_method :base64_padding_removed, 41 | :base64_padding_added 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/json_web_token/jwa.rb: -------------------------------------------------------------------------------- 1 | require 'json_web_token/algorithm/ecdsa' 2 | require 'json_web_token/algorithm/hmac' 3 | require 'json_web_token/algorithm/rsa' 4 | 5 | module JsonWebToken 6 | # Choose a cryptographic algorithm to be used for a JSON Web Signature (JWS) 7 | # @see http://tools.ietf.org/html/rfc7518 8 | module Jwa 9 | 10 | ALGORITHMS = /(HS|RS|ES)(256|384|512)?/i 11 | ALG_LENGTH = 5 12 | 13 | module_function 14 | 15 | # @param algorithm [String] 'alg' header parameter value for JWS 16 | # @param key [String | OpenSSL::PKey::RSA | OpenSSL::PKey::EC] secret key used to sign 17 | # a digital signature, or mac 18 | # @param signing_input [String] input payload for a mac computation 19 | # @return [BinaryString] a digital signature, or mac 20 | # @example 21 | # key = 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' 22 | # Jwa.sign('HS256', key, 'signing_input').bytes 23 | # # => [90, 34, 44, 252, 147, 130, 167, 173, 86, 191, 247, 93, 94, 12, 200, 30, 173, 115, 248, 89, 246, 222, 4, 213, 119, 74, 70, 20, 231, 194, 104, 103] 24 | def sign(algorithm, key, signing_input) 25 | alg_module, sha_bits = validated_alg(algorithm) 26 | alg_module.sign(sha_bits, key, signing_input) 27 | end 28 | 29 | # @param mac [BinaryString] a digital signature, or mac 30 | # @param algorithm [String] 'alg' header parameter value for JWS 31 | # @param key [String | OpenSSL::PKey::RSA | OpenSSL::PKey::EC] key used to verify 32 | # a digital signature, or mac 33 | # @param signing_input [String] input payload for a mac computation 34 | # @example 35 | # key = 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' 36 | # Jwa.verify?(< binary_string >, 'HS256', key, 'signing_input') 37 | # # => true 38 | def verify?(mac, algorithm, key, signing_input) 39 | alg_module, sha_bits = validated_alg(algorithm) 40 | alg_module.verify?(mac, sha_bits, key, signing_input) 41 | end 42 | 43 | def validated_alg(algorithm) 44 | alg = destructured_alg(algorithm) 45 | alg ? alg : fail('Unrecognized algorithm') 46 | end 47 | 48 | def destructured_alg(algorithm) 49 | match = algorithm.match(ALGORITHMS) 50 | return unless match && match[0].length == ALG_LENGTH 51 | alg_module = validated_constant(match[1].downcase) 52 | sha_bits = match[2] 53 | [alg_module, sha_bits] 54 | end 55 | 56 | def validated_constant(str) 57 | case str 58 | when 'hs' then Algorithm::Hmac 59 | when 'rs' then Algorithm::Rsa 60 | when 'es' then Algorithm::Ecdsa 61 | else fail('Unsupported algorithm') 62 | end 63 | end 64 | 65 | private_class_method :validated_alg, 66 | :destructured_alg, 67 | :validated_constant 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/json_web_token/jws.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'json_web_token/format/base64_url' 3 | require 'json_web_token/jwa' 4 | require 'json_web_token/util' 5 | 6 | module JsonWebToken 7 | # Represent content to be secured with digital signatures or Message Authentication Codes (MACs) 8 | # @see http://tools.ietf.org/html/rfc7515 9 | module Jws 10 | 11 | MESSAGE_SIGNATURE_PARTS = 3 12 | 13 | module_function 14 | 15 | # @param header [Hash] the desired set of JWS header parameters 16 | # @param payload [String] content to be used as the JWS payload 17 | # @param key [String | OpenSSL::PKey::RSA | OpenSSL::PKey::EC] secret key used to sign 18 | # a digital signature, or mac 19 | # @return [String] a JSON Web Signature, representing a digitally signed payload 20 | # @example 21 | # header = {alg: 'HS256'} 22 | # key = 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' 23 | # Jws.sign(header, 'payload', key) 24 | # # => 'eyJhbGciOiJIUzI1NiJ9.cGF5bG9hZA.uVTaOdyzp_f4mT_hfzU8LnCzdmlVC4t2itHDEYUZym4' 25 | # @see http://tools.ietf.org/html/rfc7515#page-15 26 | def sign(header, payload, key) 27 | alg = alg_parameter(header) 28 | signing_input = encode_input(header, payload) 29 | "#{signing_input}.#{signature(alg, key, signing_input)}" 30 | end 31 | 32 | # @param header [Hash] the desired set of JWS header parameters 33 | # @param payload [String] content to be used as the JWS payload 34 | # @return [String] a JWS that provides no integrity protection (i.e. lacks a signature) 35 | # @example 36 | # header = {alg: 'none'} 37 | # Jws.sign(header, 'payload') 38 | # # => 'eyJhbGciOiJub25lIn0.cGF5bG9hZA.' 39 | # @see http://tools.ietf.org/html/rfc7515#page-47 40 | def unsecured_message(header, payload) 41 | fail("Invalid 'alg' header parameter") unless alg_parameter(header) == 'none' 42 | "#{encode_input(header, payload)}." # note trailing '.' 43 | end 44 | 45 | # @param jws [String] a JSON Web Signature 46 | # @param algorithm [String] 'alg' header parameter value for JWS 47 | # @param key [String | OpenSSL::PKey::RSA | OpenSSL::PKey::EC] key used to verify 48 | # a digital signature, or mac 49 | # @return [Hash] +{ok: }+ if the mac verifies, 50 | # or +{error: 'invalid'}+ otherwise 51 | # @example 52 | # jws = 'eyJhbGciOiJIUzI1NiJ9.cGF5bG9hZA.uVTaOdyzp_f4mT_hfzU8LnCzdmlVC4t2itHDEYUZym4' 53 | # key = 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' 54 | # Jws.verify(jws, 'HS256', key) 55 | # # => {ok: 'eyJhbGciOiJIUzI1NiJ9.cGF5bG9hZA.uVTaOdyzp_f4mT_hfzU8LnCzdmlVC4t2itHDEYUZym4'} 56 | # @see http://tools.ietf.org/html/rfc7515#page-16 57 | def verify(jws, algorithm, key = nil) 58 | validate_alg_match(jws, algorithm) 59 | return {ok: jws} if algorithm == 'none' 60 | signature_verify?(jws, algorithm, key) ? {ok: jws} : {error: 'invalid'} 61 | end 62 | 63 | def alg_parameter(header) 64 | alg = Util.symbolize_keys(header)[:alg] 65 | alg && !alg.empty? ? alg : fail("Missing required 'alg' header parameter") 66 | end 67 | 68 | def encode_input(header, payload) 69 | "#{Format::Base64Url.encode(header.to_json)}.#{Format::Base64Url.encode(payload)}" 70 | end 71 | 72 | def signature(algorithm, key, data) 73 | Format::Base64Url.encode(Jwa.sign(algorithm, key, data)) 74 | end 75 | 76 | # http://tools.ietf.org/html/rfc7515#section-4.1.1 77 | def validate_alg_match(jws, algorithm) 78 | header = decoded_header_json_to_hash(jws) 79 | unless alg_parameter(header) == algorithm 80 | fail("Algorithm not matching 'alg' header parameter") 81 | end 82 | end 83 | 84 | def decoded_header_json_to_hash(jws) 85 | JSON.parse(Format::Base64Url.decode(jws.split('.')[0])) 86 | end 87 | 88 | def signature_verify?(jws, algorithm, key) 89 | ary = jws.split('.') 90 | return unless key && ary.length == MESSAGE_SIGNATURE_PARTS 91 | decoded_signature = Format::Base64Url.decode(ary[2]) 92 | payload = "#{ary[0]}.#{ary[1]}" 93 | Jwa.verify?(decoded_signature, algorithm, key, payload) 94 | end 95 | 96 | private_class_method :alg_parameter, 97 | :encode_input, 98 | :signature, 99 | :validate_alg_match, 100 | :decoded_header_json_to_hash, 101 | :signature_verify? 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/json_web_token/jwt.rb: -------------------------------------------------------------------------------- 1 | require 'json_web_token/jws' 2 | 3 | module JsonWebToken 4 | # Encode claims for transmission as a JSON object that is used as the payload of a JSON Web 5 | # Signature (JWS) structure, enabling the claims to be integrity protected with a Message 6 | # Authentication Code (MAC), to be later verified 7 | # @see http://tools.ietf.org/html/rfc7519 8 | module Jwt 9 | 10 | ALG_DEFAULT = 'HS256' 11 | HEADER_DEFAULT = { 12 | typ: 'JWT', 13 | alg: ALG_DEFAULT 14 | } 15 | 16 | module_function 17 | 18 | # @param claims [Hash] a collection of name/value pairs asserting information about a subject 19 | # @param options [Hash] specify the desired signing algorithm and signing key 20 | # (e.g String for Hmac | OpenSSL::PKey::RSA | OpenSSL::PKey::EC) 21 | # @return [String] a JSON Web Token, representing digitally signed claims 22 | # @example 23 | # claims = {iss: 'joe', exp: 1300819380, :'http://example.com/is_root' => true} 24 | # options = {alg: 'HS256', key: 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C'} 25 | # Jwt.sign(claims, options) 26 | # # => 'eyJhbGciOiJIUzI1NiJ9.cGF5bG9hZA.uVTaOdyzp_f4mT_hfzU8LnCzdmlVC4t2itHDEYUZym4' 27 | # @see http://tools.ietf.org/html/rfc7519#section-7.1 28 | def sign(claims, options) 29 | message = validated_message(claims) 30 | header = config_header(options) 31 | return Jws.unsecured_message(header, message) if header[:alg] == 'none' 32 | Jws.sign(header, message, options[:key]) 33 | end 34 | 35 | # @param jwt [String] a JSON Web Token 36 | # @param options [Hash] specify the desired verifying algorithm and verifying key 37 | # @return [Hash] +{ok: }+ if the jwt verifies, 38 | # or +{error: 'Invalid'}+ otherwise 39 | # @example 40 | # jwt = 'eyJhbGciOiJIUzI1NiJ9.cGF5bG9hZA.uVTaOdyzp_f4mT_hfzU8LnCzdmlVC4t2itHDEYUZym4' 41 | # options = {alg: 'HS256', key: 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C'} 42 | # Jwt.verify(jwt, options) 43 | # # => {ok: {iss: 'joe', exp: 1300819380, :'http://example.com/is_root' => true}} 44 | # @see see http://tools.ietf.org/html/rfc7519#section-7.2 45 | def verify(jwt, options) 46 | alg = options[:alg] || ALG_DEFAULT 47 | payload(Jws.verify(jwt, alg, options[:key])) 48 | end 49 | 50 | def validated_message(claims) 51 | fail('Claims blank') if !claims || claims.empty? 52 | claims.to_json 53 | end 54 | 55 | def config_header(options) 56 | HEADER_DEFAULT.merge(alg_parameter_required options) 57 | end 58 | 59 | def alg_parameter_required(options) 60 | hsh = options.select { |k, _v| k == :alg } # filter unsupported keys 61 | alg = hsh[:alg] 62 | alg && !alg.empty? ? hsh : {} 63 | end 64 | 65 | def payload(hsh) 66 | return {error: 'invalid'} if hsh[:error] 67 | ary = hsh[:ok].split('.') 68 | return {error: 'invalid JWS'} unless ary.length > 1 69 | encoded_claims = ary[1] 70 | {ok: payload_to_hash(encoded_claims)} 71 | end 72 | 73 | def payload_to_hash(encoded_claims) 74 | Util.symbolize_keys(JSON.parse(Format::Base64Url.decode encoded_claims)) 75 | end 76 | 77 | private_class_method :validated_message, 78 | :config_header, 79 | :alg_parameter_required, 80 | :payload, 81 | :payload_to_hash 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/json_web_token/util.rb: -------------------------------------------------------------------------------- 1 | module JsonWebToken 2 | # Utility methods 3 | module Util 4 | 5 | module_function 6 | 7 | # @param a [String] 8 | # @param b [String] 9 | # @return [Boolean] a predicate that compares two strings for equality in constant-time 10 | # to avoid timing attacks 11 | # @example 12 | # Util.constant_time_compare?("a", "A") 13 | # # => false 14 | # @see https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#section-3.2 15 | # @see cf. rails activesupport/lib/active_support/security_utils.rb 16 | def constant_time_compare?(a, b) 17 | return false if a.nil? || b.nil? || a.empty? || b.empty? 18 | secure_compare(a, b) 19 | end 20 | 21 | # @param hsh [Hash] 22 | # @return [Hash] a new hash with all keys converted to symbols, 23 | # as long as they respond to to_sym 24 | # @example 25 | # Util.symbolize_keys({'a' => 0, 'b' => '2', c: '3'}) 26 | # # => {a: 0, b: '2', c: '3'} 27 | # @see cf. rails activesupport/lib/active_support/core_ext/hash/keys.rb 28 | def symbolize_keys(hsh) 29 | transform_keys(hsh) { |key| key.to_sym rescue key } 30 | end 31 | 32 | def secure_compare(a, b) 33 | return false unless a.bytesize == b.bytesize 34 | l = a.unpack "C#{a.bytesize}" 35 | res = 0 36 | b.each_byte { |byte| res |= byte ^ l.shift } 37 | res == 0 38 | end 39 | 40 | def transform_keys(hsh) 41 | result = Hash.new 42 | hsh.keys.each { |k| result[yield(k)] = hsh[k] } 43 | result 44 | end 45 | 46 | private_class_method :secure_compare, 47 | :transform_keys 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/json_web_token/version.rb: -------------------------------------------------------------------------------- 1 | module JsonWebToken 2 | VERSION = "0.3.5" 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/rsa/private_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAruE38DIgPqG4sJeFfXDS80BZdCoQRakRmKfkoOq26BaCerNK 3 | IsMV1zMCnFWh/c3ax7iWXMbIrUIixDxAjOe/smzRCaT9rL6Jz235dbYX81mdGPUA 4 | /PqzgwbfYnoKf9tDrUWDG0oU9WN/HJDY+1lKFW3sG29ENsLn7ezJOP8d7y1mIocg 5 | l94dfKKaPlM3NECPObXGbJWyPsIlYrgCBKwdH6zHfKFeDDawsagtK7MZfzVfh/bI 6 | JzL+84oCOUn75pXwl5JKOLaudYuHFNNGe4XCma+l0BEyfIAykIm2V5tL2zUaEPm6 7 | Mh8r70nEeLtEDUgbVXk6OoEVfbEksvRfEaxPUQIDAQABAoIBAE2n+R0SRTtKuSbB 8 | widX6HTYSGDLJPqp/wRY2a+TSrF4/WvbHbqeAp7TKf+eDxcTSGmT0EFWjFpTmfim 9 | AMwRRRDIPXMY7wS8f5m67iGVQMFxM8+XTF8KPJicfqnOI8du6HiS+4sstG3bRuC6 10 | eX/zwHAPpd7w48uldTQ0B47lyrcgJ8ZaKdA7BZRzxSojSrCmosajoOoAqRUKj9Bw 11 | CcDVmZOrgd0z2spNsP/h1u8tyKNc55QtY7AbhiuJ7Eo99Fob78xH7pKgZk10MqLX 12 | xWB0x5yKAptJ5O/cGkHwJ0CNP+/bWN4tRPJLd7q+lTDvBf40ix3UJ8xq2MItJoIo 13 | 4ysKplkCgYEA22tUs30r8qtQwzkTnwKnnNtHQbCVNx79PbQiSVeg1OFULBJ9Q5JB 14 | tScgTeINvjeYvFk5yguy25dUZfpLy8tR2mziGy023LTfnPM7ldsFsoXgW+TimQhd 15 | xn4aizgLk4F+jmX+Ga/YJllSUBmzgHtrJxFyFgfprlCWY8Oy6ayQvLMCgYEAzAj4 16 | nPjf0tIU+HLVQNxzq+6EaJIdsaF4VNtuc5RUyIpN7ptnm+Qp1sZgMTFuZY/NGUJd 17 | JUHF2598P7izf8k1U55CzHt9WR3gXADiDgubWXKJC9aO67RXQPfUX0f8g0MBpRyr 18 | aMb1P3vEeEN6Mz2+rpxRQ+VMZHm03WgXfKTeDesCgYBw13nv9OetHT2jqK7Wj97z 19 | ZDH5Ln8OoflKymD3pwEnedp37+pL3eO87ipG44J92MnL03dM2UdadzEIYPiqDh5C 20 | XszJfS9BwiNd7BkwsDr13LPNjqCaYxsXAbNU9bW+XlJfmURcoXpf/n++gfk2kkJB 21 | DU/G1Lrw0rEFDZftZLnq0QKBgQC4vsLF939K+Bgu4mN/6F0AZ+cSfINQjKFVTJ8V 22 | HlEqiDiKfcJx4EWWCGefA2Avq+aNIY8Pph+OfjlVxVsasMiNXzaiyoo0QB4fkFkf 23 | WJkgg9ndkzgz6kY7OGvenmLhQ7HgnDYEmGoGQn4iWw7yCM14c2gN7zidnaoL9C2Y 24 | GfxKYwKBgQCvOp4mlfL176dKrCjOxVFqnpXMCS3WqTWk5e+uR6GKcs6SvEfue0X0 25 | B/SVRn6tyKqLp9pvM+0fEaXrspZmgoedmiPY3V7Am5bdMOltW5DMvxAAdnqjok1i 26 | q2O8Of/DTt2bn3nu8IZDeHrKebx8z/CG9rQK4x5fT1HcwAgroYG0bQ== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /spec/fixtures/rsa/private_key_weak.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEgQIBAAKB+wDZmyrApkiH+zL7G2Gzwht9N0KbW/iJCeygAKyqHoc75nFLE4We 3 | jxGHR4RlIg9kYn1Qbm7Yl5/ASFIE//vMjqyJJ5GqoeVAKZfAp4NFKd99Ja5z1pIW 4 | /hh4n2fswF9VYRsuCC0nmDQ9XNUZunXVJPJwOZ3RlWkE1yIbU1Ianr+Y81SkF6bp 5 | pT/ucmA52RbVImoDoxztu0OK7CQ/yE3+gvHSwLaRE3/s5NpGhOPj3XoumMZoyAyb 6 | rWlBCadAoaaaPAZx460xTP+I6n6CDM6/6uPazaRbOVxChIgiOxpPU39aNKwbB/iN 7 | 7LOuiSO1mStLXVTVpCsQukJa6GBPAgMBAAECgfo4oBYp95tcN1P24BM2kcZReTXN 8 | 88ri7kAzCrnjsvoB7l+A592XeydWipe7MXfL6Y0uhu41Uo9xm+AsCOjfPClNr4xK 9 | WMKtRaWFrpGMR+8uQ8X3wthVP13i1ahXhV/JefF2sw39BbBiX2vcvBa5llm4NmYJ 10 | CdZAOjNX5QCyiC3qDkYrQnY3c8ccWA+SdrZylRRBufzKxbp0HPU7NpQJmKGrORTd 11 | lkYgXKI7QPMQ1/LgaVtEbG77MYbmzt0PAw6w5nQlwF8G9hV37Y6xanpg3EobOih2 12 | qM/qmWRyg417h1VTMxVQLNf53f9P/nzRnSQK79ZDzPmSyI+ww0kRAn4A9TjM/51r 13 | c/1IlFrWpo3NlsepGBmyX0DUNWuZOhA4Z2z7+GyAokF0LacFRyQNxzwFLAuMargm 14 | WMgAKNs3LaZxrsWNCbyUCv2vyGSREsFJtXRIs2jBE4TUycOZvtgD32Y0EeKknc9y 15 | RCT/kPSkKj/QDB8vsRaUql7829uuVAsCfgDjK6NPv8fTyLITPeCJoOcCa4LDr6nZ 16 | vB21FCJom2eumrQ6lHkuLiFh2E+E2ICvKuCBaNi5g01wV6WnjJ1Ou3r33Ig+H6wX 17 | La7cr0bq+QlLQy/woBlrgevIseAVKFnBe/rB8RY3vLTV53DLsRIlUjW3r0Hi+ovN 18 | 6ky/6dnrTQJ9cHBk/ufNd1exyeSE+8WVeQd/rz9JHIf7RatajNcXJcVBG72UldFx 19 | VeV5dA9CChKpRHLfJafeXWMHP7SUbmgdREfszYQcVZXQ3g/OvUzHYV46X5kuvqZv 20 | czwu6OTfkbWpNnI3jOyrtjdIv8yM4Zp06Ln8AjUaYaxAQQdPvdUCfgC6qdHQptpw 21 | 6HmCnBaToFYRw/u4BfDAC/YxI7E+WWvxqgcsF3IXgypE9vQoSao7xTSiwV9L2Fta 22 | U3q32TAtUJozbs+4lFfY193WUmQokaeUY0nSJAufLitrRtXFLNnLxa7/i7Vg6TuC 23 | hGFHzmEFr66aF3VBJ/SWp86W53X8BQJ9R6IhbZpxnGW3TbcgP5R7xVBcas+ypYn6 24 | CEV22CzMDSE21Zek/q0N65im9QJgHrtLXSrb6DVOJF6lp8pm3yO+UXsaMTldysvo 25 | H7sN9CzhhQNpSwFp4dvYQBi6pIQGsit6lbOj5wkE3PeRsusO6pcIli2w2MzgPjxn 26 | OERK2BM= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /spec/fixtures/rsa/public_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAruE38DIgPqG4sJeFfXDS 3 | 80BZdCoQRakRmKfkoOq26BaCerNKIsMV1zMCnFWh/c3ax7iWXMbIrUIixDxAjOe/ 4 | smzRCaT9rL6Jz235dbYX81mdGPUA/PqzgwbfYnoKf9tDrUWDG0oU9WN/HJDY+1lK 5 | FW3sG29ENsLn7ezJOP8d7y1mIocgl94dfKKaPlM3NECPObXGbJWyPsIlYrgCBKwd 6 | H6zHfKFeDDawsagtK7MZfzVfh/bIJzL+84oCOUn75pXwl5JKOLaudYuHFNNGe4XC 7 | ma+l0BEyfIAykIm2V5tL2zUaEPm6Mh8r70nEeLtEDUgbVXk6OoEVfbEksvRfEaxP 8 | UQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /spec/fixtures/rsa/public_key_alt.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApcDymjKwuWwyq9Zyt7EF 3 | epTHzyvO875ioqDoMtCNhADrjLU25vaAaCyyf035VoXl/HYaUj0tBiEvQreso98O 4 | eGO6cnfnu/QBvd1veyKtA8EjHFeo7NeA/RdXj6sEWtdM+QVHy6yI8YS/WAu8Ubrb 5 | MNeDk4dEmfvvgL9TxNAYuVlWcLuR1fEyNN4NsansHZtoBCf6lRhd6l9uvu1HZJJ/ 6 | UrOostwH5jgnI3rWS2fB9SU7G5l9+YS7NbNGxa4kE6x72ko5WPg1U+wch94rZhxT 7 | XBJDipRIox83wEQEP0qH8IWC905CmnYqfZ7HMdGt7KWvg/svO/q4oRtCb5NDEDGL 8 | 6QIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /spec/json_web_token/algorithm/ecdsa_spec.rb: -------------------------------------------------------------------------------- 1 | require 'json_web_token/algorithm/ecdsa' 2 | require 'support/ecdsa_key' 3 | 4 | module JsonWebToken 5 | module Algorithm 6 | describe Ecdsa do 7 | let(:signing_input_0) { '{"iss":"joe","exp":1300819380,"http://example.com/is_root":true}' } 8 | let(:signing_input_1) { '{"iss":"mike","exp":1300819380,"http://example.com/is_root":false}' } 9 | describe 'detect changed signature or data' do 10 | shared_examples_for '#sign' do 11 | it 'does #verify?' do 12 | private_key_0 = EcdsaKey.curve_new(sha_bits) 13 | public_key_str_0 = EcdsaKey.public_key_str(private_key_0) 14 | public_key_0 = EcdsaKey.public_key_new(sha_bits, public_key_str_0) 15 | 16 | mac_0 = Ecdsa.sign(sha_bits, private_key_0, signing_input_0) 17 | expect(mac_0.bytes.count).to eql expected_mac_byte_count 18 | expect(Ecdsa.verify? mac_0, sha_bits, public_key_0, signing_input_0).to be true 19 | 20 | private_key_1 = EcdsaKey.curve_new(sha_bits) 21 | public_key_str_1 = EcdsaKey.public_key_str(private_key_1) 22 | public_key_1 = EcdsaKey.public_key_new(sha_bits, public_key_str_1) 23 | 24 | expect(Ecdsa.verify? mac_0, sha_bits, public_key_0, signing_input_1).to be false 25 | expect(Ecdsa.verify? mac_0, sha_bits, public_key_1, signing_input_0).to be false 26 | expect(Ecdsa.verify? mac_0, sha_bits, public_key_1, signing_input_1).to be false 27 | 28 | mac_1 = Ecdsa.sign(sha_bits, private_key_1, signing_input_1) 29 | expect(Ecdsa.verify? mac_1, sha_bits, public_key_0, signing_input_0).to be false 30 | expect(Ecdsa.verify? mac_1, sha_bits, public_key_0, signing_input_1).to be false 31 | expect(Ecdsa.verify? mac_1, sha_bits, public_key_1, signing_input_0).to be false 32 | expect(Ecdsa.verify? mac_1, sha_bits, public_key_1, signing_input_1).to be true 33 | end 34 | end 35 | 36 | describe 'ES256' do 37 | let(:sha_bits) { '256' } 38 | let(:expected_mac_byte_count) { 64 } 39 | it_behaves_like '#sign' 40 | end 41 | 42 | describe 'ES384' do 43 | let(:sha_bits) { '384' } 44 | let(:expected_mac_byte_count) { 96 } 45 | it_behaves_like '#sign' 46 | end 47 | 48 | describe 'ES512' do 49 | let(:sha_bits) { '512' } 50 | let(:expected_mac_byte_count) { 132 } 51 | it_behaves_like '#sign' 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/json_web_token/algorithm/hmac_spec.rb: -------------------------------------------------------------------------------- 1 | require 'json_web_token/algorithm/hmac' 2 | 3 | module JsonWebToken 4 | module Algorithm 5 | describe Hmac do 6 | let(:signing_input_0) { '{"iss":"joe","exp":1300819380,"http://example.com/is_root":true}' } 7 | let(:signing_input_1) { '{"iss":"mike","exp":1300819380,"http://example.com/is_root":false}' } 8 | context 'detect changed signing_input or MAC' do 9 | shared_examples_for '#sign' do 10 | it 'does #verify?' do 11 | mac = Hmac.sign(sha_bits, shared_key, signing_input_0) 12 | expect(Hmac.verify? mac, sha_bits, shared_key, signing_input_0).to be true 13 | expect(Hmac.verify? mac, sha_bits, shared_key, signing_input_1).to be false 14 | 15 | changed_mac = Hmac.sign(sha_bits, shared_key, signing_input_1) 16 | expect(Hmac.verify? changed_mac, sha_bits, shared_key, signing_input_0).to be false 17 | end 18 | end 19 | 20 | describe 'HS256' do 21 | let(:sha_bits) { '256' } 22 | let(:shared_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' } 23 | it_behaves_like '#sign' 24 | end 25 | 26 | describe 'HS384' do 27 | let(:sha_bits) { '384' } 28 | let(:shared_key) { 'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS' } 29 | it_behaves_like '#sign' 30 | end 31 | 32 | describe 'HS512' do 33 | let(:sha_bits) { '512' } 34 | let(:shared_key) { 'ysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hc' } 35 | it_behaves_like '#sign' 36 | end 37 | end 38 | 39 | describe 'changed key' do 40 | let(:sha_bits) { '256' } 41 | let(:shared_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' } 42 | let(:changed_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9Z' } 43 | it 'fails to #verify?' do 44 | mac = Hmac.sign(sha_bits, shared_key, signing_input_0) 45 | expect(Hmac.verify? mac, sha_bits, shared_key, signing_input_0).to be true 46 | expect(Hmac.verify? mac, sha_bits, changed_key, signing_input_0).to be false 47 | end 48 | end 49 | 50 | context 'param validation' do 51 | shared_examples_for 'invalid key' do 52 | it 'raises' do 53 | expect { Hmac.sign(sha_bits, shared_key, signing_input_0) } 54 | .to raise_error(RuntimeError, 'Invalid shared key') 55 | end 56 | end 57 | 58 | context 'w 256 sha_bits' do 59 | let(:sha_bits) { '256' } 60 | describe 'shared_key nil' do 61 | let(:shared_key) { nil } 62 | it_behaves_like 'invalid key' 63 | end 64 | 65 | describe "shared_key 'empty string'" do 66 | let(:shared_key) { '' } 67 | it_behaves_like 'invalid key' 68 | end 69 | 70 | describe 'shared_key length (31) < MAC length (32)' do 71 | let(:shared_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9' } 72 | it_behaves_like 'invalid key' 73 | end 74 | 75 | describe 'shared_key length (32) == MAC length (32)' do 76 | let(:shared_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' } 77 | it 'returns a 32-byte MAC string' do 78 | mac = Hmac.sign(sha_bits, shared_key, signing_input_0) 79 | expect(mac.bytesize).to eql 32 80 | end 81 | end 82 | end 83 | 84 | context 'w 384 sha_bits' do 85 | let(:sha_bits) { '384' } 86 | describe 'shared_key length (47) < MAC length (48)' do 87 | let(:shared_key) { 'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1q' } 88 | it_behaves_like 'invalid key' 89 | end 90 | 91 | describe 'shared_key length (48) == MAC length (48)' do 92 | let(:shared_key) { 'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS' } 93 | it 'returns a 48-byte MAC string' do 94 | mac = Hmac.sign(sha_bits, shared_key, signing_input_0) 95 | expect(mac.bytesize).to eql 48 96 | end 97 | end 98 | end 99 | 100 | context 'w 512 sha_bits' do 101 | let(:sha_bits) { '512' } 102 | describe 'shared_key length (63) < MAC length (64)' do 103 | let(:shared_key) { 'ysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4h' } 104 | it_behaves_like 'invalid key' 105 | end 106 | 107 | describe 'shared_key length (64) == MAC length (64)' do 108 | let(:shared_key) { 'ysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hc' } 109 | it 'returns a 64-byte MAC string' do 110 | mac = Hmac.sign(sha_bits, shared_key, signing_input_0) 111 | expect(mac.bytesize).to eql 64 112 | end 113 | end 114 | end 115 | 116 | describe 'w unrecognized sha_bits' do 117 | let(:sha_bits) { '257' } 118 | let(:shared_key) { 'ysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hc' } 119 | it 'raises' do 120 | expect { Hmac.sign(sha_bits, shared_key, signing_input_0) } 121 | .to raise_error(RuntimeError, 'Invalid sha_bits') 122 | end 123 | end 124 | end 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /spec/json_web_token/algorithm/rsa_spec.rb: -------------------------------------------------------------------------------- 1 | require 'json_web_token/algorithm/rsa' 2 | require 'json_web_token/algorithm/rsa_util' 3 | 4 | module JsonWebToken 5 | module Algorithm 6 | describe Rsa do 7 | let(:signing_input_0) { '{"iss":"joe","exp":1300819380,"http://example.com/is_root":true}' } 8 | let(:signing_input_1) { '{"iss":"mike","exp":1300819380,"http://example.com/is_root":false}' } 9 | let(:path_to_keys) { 'spec/fixtures/rsa' } 10 | context 'detect changed signing_input or MAC' do 11 | let(:private_key) { RsaUtil.private_key(path_to_keys) } 12 | let(:public_key) { RsaUtil.public_key(path_to_keys) } 13 | shared_examples_for '#sign' do 14 | it 'does #verify?' do 15 | mac = Rsa.sign(sha_bits, private_key, signing_input_0) 16 | expect(Rsa.verify? mac, sha_bits, public_key, signing_input_0).to be true 17 | expect(Rsa.verify? mac, sha_bits, public_key, signing_input_1).to be false 18 | 19 | changed_mac = Rsa.sign(sha_bits, private_key, signing_input_1) 20 | expect(Rsa.verify? changed_mac, sha_bits, public_key, signing_input_0).to be false 21 | end 22 | end 23 | 24 | context 'RS256' do 25 | let(:sha_bits) { '256' } 26 | it_behaves_like '#sign' 27 | 28 | describe 'changed key' do 29 | let(:changed_public_key) { RsaUtil.public_key(path_to_keys, 'public_key_alt.pem') } 30 | it 'fails to #verify?' do 31 | mac = Rsa.sign(sha_bits, private_key, signing_input_0) 32 | expect(Rsa.verify? mac, sha_bits, public_key, signing_input_0).to be true 33 | expect(Rsa.verify? mac, sha_bits, changed_public_key, signing_input_0).to be false 34 | end 35 | end 36 | end 37 | 38 | describe 'RS384' do 39 | let(:sha_bits) { '384' } 40 | it_behaves_like '#sign' 41 | end 42 | 43 | describe 'RS512' do 44 | let(:sha_bits) { '512' } 45 | it_behaves_like '#sign' 46 | end 47 | end 48 | 49 | context 'param validation' do 50 | shared_examples_for 'invalid private_key' do 51 | it 'raises' do 52 | expect { Rsa.sign(sha_bits, private_key, signing_input_0) } 53 | .to raise_error(RuntimeError, 'Invalid key: RSA modulus too small') 54 | end 55 | end 56 | 57 | context 'private_key bit size < KEY_BITS_MIN (2048)' do 58 | let(:private_key) { RsaUtil.private_key(path_to_keys, 'private_key_weak.pem') } 59 | describe 'w 256 sha_bits' do 60 | let(:sha_bits) { '256' } 61 | it_behaves_like 'invalid private_key' 62 | end 63 | 64 | describe 'w 384 sha_bits' do 65 | let(:sha_bits) { '384' } 66 | it_behaves_like 'invalid private_key' 67 | end 68 | 69 | describe 'w 512 sha_bits' do 70 | let(:sha_bits) { '512' } 71 | it_behaves_like 'invalid private_key' 72 | end 73 | end 74 | 75 | shared_examples_for '2048 bit private_key' do 76 | it 'returns a 256-byte MAC string' do 77 | mac = Rsa.sign(sha_bits, private_key, signing_input_0) 78 | expect(mac.bytesize).to eql 256 79 | end 80 | end 81 | 82 | context 'private_key bits (2048) == KEY_BITS_MIN (2048)' do 83 | let(:private_key) { RsaUtil.private_key(path_to_keys) } 84 | describe 'w 256 sha_bits' do 85 | let(:sha_bits) { '256' } 86 | it_behaves_like '2048 bit private_key' 87 | end 88 | 89 | describe 'w 384 sha_bits' do 90 | let(:sha_bits) { '384' } 91 | it_behaves_like '2048 bit private_key' 92 | end 93 | 94 | describe 'w 512 sha_bits' do 95 | let(:sha_bits) { '512' } 96 | it_behaves_like '2048 bit private_key' 97 | end 98 | end 99 | 100 | context 'blank private_key' do 101 | let(:sha_bits) { '256' } 102 | describe 'nil' do 103 | let(:private_key) { nil } 104 | it_behaves_like 'invalid private_key' 105 | end 106 | 107 | describe 'empty string' do 108 | let(:private_key) { '' } 109 | it 'raises' do 110 | expect { Rsa.sign(sha_bits, private_key, signing_input_0) }.to raise_error(NoMethodError) 111 | end 112 | end 113 | end 114 | 115 | describe 'w unrecognized sha_bits' do 116 | let(:sha_bits) { '257' } 117 | let(:private_key) { 'private_key' } 118 | it 'raises' do 119 | expect { Rsa.sign(sha_bits, private_key, signing_input_0) } 120 | .to raise_error(RuntimeError, 'Invalid sha_bits') 121 | end 122 | end 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /spec/json_web_token/format/asn1_spec.rb: -------------------------------------------------------------------------------- 1 | require 'json_web_token/format/asn1' 2 | 3 | module JsonWebToken 4 | module Format 5 | describe Asn1 do 6 | context 'w bytes' do 7 | let(:der) { der_bytes.map(&:chr).join } 8 | let(:signature) { signature_bytes.map(&:chr).join } 9 | shared_examples_for '#der_to_signature' do 10 | it 'converts' do 11 | expect(signature.bytes.length).to eql signature_byte_count 12 | expect(Asn1.der_to_signature(der, sha_bits).bytes).to eql signature_bytes 13 | end 14 | end 15 | 16 | shared_examples_for '#signature_to_der' do 17 | it 'converts' do 18 | expect(Asn1.signature_to_der(signature, sha_bits).bytes).to eql der_bytes 19 | end 20 | end 21 | 22 | shared_examples_for 'w/o valid signature' do 23 | let(:signature_invalid) { (signature_bytes + [123]).map(&:chr).join } 24 | it '#signature_to_der raises' do 25 | expect { Asn1.signature_to_der(signature_invalid, sha_bits) } 26 | .to raise_error(RuntimeError, 'Invalid signature length') 27 | end 28 | end 29 | 30 | context 'for ES256' do 31 | let(:sha_bits) { '256' } 32 | let(:der_bytes) { [48, 69, 2, 32, 39, 115, 251, 5, 254, 60, 42, 53, 128, 68, 123, 82, 33 | 222, 136, 26, 167, 246, 163, 233, 216, 206, 122, 106, 141, 43, 143, 137, 3, 88, 196, 34 | 235, 161, 2, 33, 0, 143, 213, 54, 244, 194, 216, 188, 161, 77, 28, 87, 205, 16, 160, 35 | 11, 125, 21, 62, 206, 233, 242, 201, 149, 152, 53, 25, 103, 6, 4, 56, 193, 161] } 36 | let(:signature_bytes) { [39, 115, 251, 5, 254, 60, 42, 53, 128, 68, 123, 82, 222, 136, 37 | 26, 167, 246, 163, 233, 216, 206, 122, 106, 141, 43, 143, 137, 3, 88, 196, 235, 161, 38 | 143, 213, 54, 244, 194, 216, 188, 161, 77, 28, 87, 205, 16, 160, 11, 125, 21, 62, 39 | 206, 233, 242, 201, 149, 152, 53, 25, 103, 6, 4, 56, 193, 161] } 40 | let(:signature_byte_count) { 64 } 41 | it_behaves_like '#der_to_signature' 42 | it_behaves_like '#signature_to_der' 43 | it_behaves_like 'w/o valid signature' 44 | 45 | describe 'invalid sha_bits' do 46 | let(:invalid_sha_bits) { '257' } 47 | it '#der_to_signature raises' do 48 | expect { Asn1.der_to_signature(der, invalid_sha_bits) } 49 | .to raise_error(RuntimeError, 'Invalid sha_bits') 50 | end 51 | 52 | it '#signature_to_der raises' do 53 | expect { Asn1.signature_to_der(signature, invalid_sha_bits) } 54 | .to raise_error(RuntimeError, 'Invalid sha_bits') 55 | end 56 | end 57 | end 58 | 59 | context 'for ES384' do 60 | let(:sha_bits) { '384' } 61 | let(:der_bytes) { [48, 101, 2, 48, 22, 221, 123, 224, 5, 100, 163, 31, 98, 78, 240, 62 | 249, 85, 126, 120, 130, 228, 123, 69, 2, 21, 65, 249, 229, 151, 208, 186, 162, 31, 63 | 149, 42, 165, 134, 214, 197, 176, 120, 10, 205, 247, 176, 19, 2, 156, 112, 89, 58, 64 | 234, 2, 49, 0, 255, 43, 120, 92, 206, 84, 88, 29, 109, 225, 254, 162, 37, 255, 127, 65 | 231, 37, 178, 36, 173, 225, 201, 121, 154, 43, 122, 229, 114, 50, 83, 69, 243, 143, 66 | 248, 89, 109, 136, 233, 223, 148, 137, 226, 96, 78, 166, 141, 222, 236] } 67 | let(:signature_bytes) { [22, 221, 123, 224, 5, 100, 163, 31, 98, 78, 240, 249, 85, 68 | 126, 120, 130, 228, 123, 69, 2, 21, 65, 249, 229, 151, 208, 186, 162, 31, 149, 42, 69 | 165, 134, 214, 197, 176, 120, 10, 205, 247, 176, 19, 2, 156, 112, 89, 58, 234, 255, 70 | 43, 120, 92, 206, 84, 88, 29, 109, 225, 254, 162, 37, 255, 127, 231, 37, 178, 36, 71 | 173, 225, 201, 121, 154, 43, 122, 229, 114, 50, 83, 69, 243, 143, 248, 89, 109, 136, 72 | 233, 223, 148, 137, 226, 96, 78, 166, 141, 222, 236] } 73 | let(:signature_byte_count) { 96 } 74 | it_behaves_like '#der_to_signature' 75 | it_behaves_like '#signature_to_der' 76 | it_behaves_like 'w/o valid signature' 77 | end 78 | 79 | context 'for ES512' do 80 | let(:sha_bits) { '512' } 81 | let(:der_bytes) { [48, 129, 135, 2, 66, 0, 173, 236, 131, 242, 12, 189, 123, 8, 129, 82 | 2, 239, 202, 73, 168, 134, 216, 173, 241, 30, 1, 216, 177, 69, 61, 2, 196, 126, 145, 83 | 132, 172, 174, 210, 133, 191, 50, 57, 239, 229, 201, 118, 197, 62, 197, 62, 128, 84 | 143, 82, 84, 251, 80, 18, 196, 194, 198, 62, 144, 16, 149, 26, 67, 3, 215, 235, 179, 85 | 146, 2, 65, 40, 137, 198, 254, 15, 50, 214, 252, 43, 65, 203, 163, 140, 204, 66, 86 | 159, 53, 125, 184, 29, 24, 189, 249, 21, 64, 109, 87, 100, 165, 139, 83, 129, 190, 87 | 121, 180, 86, 241, 83, 238, 39, 63, 25, 247, 253, 130, 153, 47, 27, 138, 164, 221, 88 | 25, 151, 135, 144, 84, 240, 46, 59, 94, 99, 147, 138, 103, 67] } 89 | let(:signature_bytes) { [0, 173, 236, 131, 242, 12, 189, 123, 8, 129, 2, 239, 202, 73, 90 | 168, 134, 216, 173, 241, 30, 1, 216, 177, 69, 61, 2, 196, 126, 145, 132, 172, 174, 91 | 210, 133, 191, 50, 57, 239, 229, 201, 118, 197, 62, 197, 62, 128, 143, 82, 84, 251, 92 | 80, 18, 196, 194, 198, 62, 144, 16, 149, 26, 67, 3, 215, 235, 179, 146, 0, 40, 137, 93 | 198, 254, 15, 50, 214, 252, 43, 65, 203, 163, 140, 204, 66, 159, 53, 125, 184, 29, 94 | 24, 189, 249, 21, 64, 109, 87, 100, 165, 139, 83, 129, 190, 121, 180, 86, 241, 83, 95 | 238, 39, 63, 25, 247, 253, 130, 153, 47, 27, 138, 164, 221, 25, 151, 135, 144, 84, 96 | 240, 46, 59, 94, 99, 147, 138, 103, 67] } 97 | let(:signature_byte_count) { 132 } 98 | it_behaves_like '#der_to_signature' 99 | it_behaves_like '#signature_to_der' 100 | it_behaves_like 'w/o valid signature' 101 | end 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/json_web_token/format/base64_url_spec.rb: -------------------------------------------------------------------------------- 1 | require 'json_web_token/format/base64_url' 2 | 3 | module JsonWebToken 4 | module Format 5 | describe Base64Url do 6 | context '#encode' do 7 | shared_examples_for 'w #decode' do 8 | it 'matches' do 9 | encoded = Base64Url.encode(str) 10 | expect(Base64Url.decode encoded).to eql str 11 | end 12 | end 13 | 14 | describe 'typical' do 15 | let(:str) { '{"typ":"JWT", "alg":"HS256"}' } 16 | it_behaves_like 'w #decode' 17 | end 18 | 19 | describe 'w whitespace' do 20 | let(:str) { '{"typ" :"JWT" , "alg" :"HS256" }' } 21 | it_behaves_like 'w #decode' 22 | end 23 | 24 | describe 'w line feed and carriage return' do 25 | let(:str) { '{"typ":"JWT",/n "a/rlg":"HS256"}' } 26 | it_behaves_like 'w #decode' 27 | end 28 | 29 | shared_examples_for 'given encoding' do 30 | it 'matches' do 31 | expect(Base64Url.encode str).to eql encoded 32 | expect(Base64Url.decode encoded).to eql str 33 | end 34 | end 35 | 36 | describe 'w no padding char' do 37 | let(:str) { '{"typ":"JWT", "alg":"none"}' } 38 | let(:encoded) { 'eyJ0eXAiOiJKV1QiLCAiYWxnIjoibm9uZSJ9'} 39 | it_behaves_like 'given encoding' 40 | end 41 | 42 | context 'w 1 padding char' do 43 | let(:str) { '{"typ":"JWT", "alg":"algorithm"}' } 44 | 45 | describe 'present' do 46 | let(:encoded) { 'eyJ0eXAiOiJKV1QiLCAiYWxnIjoiYWxnb3JpdGhtIn0='} 47 | it 'matches' do 48 | expect(Base64Url.decode encoded).to eql str 49 | end 50 | end 51 | 52 | describe 'removed' do 53 | let(:encoded) { 'eyJ0eXAiOiJKV1QiLCAiYWxnIjoiYWxnb3JpdGhtIn0'} 54 | it_behaves_like 'given encoding' 55 | end 56 | end 57 | 58 | context 'w 2 padding char' do 59 | let(:str) { '{"typ":"JWT", "alg":"HS256"}' } 60 | 61 | describe 'present' do 62 | let(:encoded) { 'eyJ0eXAiOiJKV1QiLCAiYWxnIjoiSFMyNTYifQ=='} 63 | it 'matches' do 64 | expect(Base64Url.decode encoded).to eql str 65 | end 66 | end 67 | 68 | describe 'removed' do 69 | let(:encoded) { 'eyJ0eXAiOiJKV1QiLCAiYWxnIjoiSFMyNTYifQ'} 70 | it_behaves_like 'given encoding' 71 | end 72 | end 73 | 74 | describe 'invalid encoding' do 75 | let(:encoded) { 'InR5cCI6IkpXVCIsICJhbGciOiJub25lI'} 76 | it 'raises' do 77 | expect { Base64Url.decode(encoded) } 78 | .to raise_error(RuntimeError, 'Invalid base64 string') 79 | end 80 | end 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/json_web_token/jwa_spec.rb: -------------------------------------------------------------------------------- 1 | require 'json_web_token/algorithm/rsa_util' 2 | require 'json_web_token/jwa' 3 | require 'support/ecdsa_key' 4 | 5 | module JsonWebToken 6 | 7 | RsaUtil = JsonWebToken::Algorithm::RsaUtil 8 | 9 | describe Jwa do 10 | let(:signing_input) { '{"iss":"joe","exp":1300819380,"http://example.com/is_root":true}' } 11 | shared_examples_for 'w #verify?' do 12 | it 'true' do 13 | expect(Jwa.verify? mac, algorithm, verifying_key, signing_input).to be true 14 | end 15 | end 16 | context '#sign' do 17 | let(:mac) { Jwa.sign(algorithm, signing_key, signing_input) } 18 | describe 'HS256' do 19 | let(:algorithm) { 'HS256' } 20 | let(:signing_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' } 21 | let(:verifying_key) { signing_key } 22 | it_behaves_like 'w #verify?' 23 | 24 | it 'returns a 32-byte MAC' do 25 | expect(mac.bytesize).to eql 32 26 | end 27 | end 28 | 29 | describe 'RS256' do 30 | let(:algorithm) { 'RS256' } 31 | let(:path_to_keys) { 'spec/fixtures/rsa' } 32 | let(:signing_key) { RsaUtil.private_key(path_to_keys) } 33 | let(:verifying_key) { RsaUtil.public_key(path_to_keys) } 34 | it_behaves_like 'w #verify?' 35 | 36 | it 'returns a 256-byte MAC' do 37 | expect(mac.bytesize).to eql 256 38 | end 39 | end 40 | 41 | describe 'ES256' do 42 | let(:algorithm) { 'ES256' } 43 | it 'w #verify? true, returns a 64-byte MAC' do 44 | private_key = EcdsaKey.curve_new('256') 45 | public_key_str = EcdsaKey.public_key_str(private_key) 46 | public_key = EcdsaKey.public_key_new('256', public_key_str) 47 | 48 | mac = Jwa.sign(algorithm, private_key, signing_input) 49 | expect(Jwa.verify? mac, algorithm, public_key, signing_input).to be true 50 | 51 | expect(mac.bytesize).to eql 64 52 | end 53 | end 54 | end 55 | 56 | context 'param validation' do 57 | context 'w HS256 key' do 58 | let(:shared_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' } 59 | describe 'unrecognized algorithm' do 60 | ['HT256', 'HS257', '', nil].each do |elt| 61 | let(:algorithm) { "#{elt}" } 62 | it 'raises' do 63 | expect { Jwa.sign(algorithm, shared_key, signing_input) } 64 | .to raise_error(RuntimeError, 'Unrecognized algorithm') 65 | end 66 | end 67 | end 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/json_web_token/jws_spec.rb: -------------------------------------------------------------------------------- 1 | require 'json_web_token/jws' 2 | require 'support/ecdsa_key' 3 | 4 | module JsonWebToken 5 | describe Jws do 6 | context 'w payload' do 7 | let(:payload) { '{"iss":"joe","exp":1300819380,"http://example.com/is_root":true}' } 8 | context '#sign' do 9 | shared_examples_for 'does #verify' do 10 | it 'w a jws' do 11 | jws = Jws.sign(header, payload, signing_key) 12 | expect(Jws.verify jws, algorithm, verifying_key).to include({ok: jws}) 13 | end 14 | end 15 | 16 | context 'w HS256 keys' do 17 | let(:signing_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' } 18 | let(:verifying_key) { signing_key } 19 | context "w HS256 'alg' header parameter" do 20 | let(:header) { {alg: 'HS256'} } 21 | context 'w passing a matching algorithm to #verify' do 22 | let(:algorithm) { 'HS256' } 23 | it_behaves_like 'does #verify' 24 | 25 | describe 'w/o passing key to #verify' do 26 | it 'returns error' do 27 | jws = Jws.sign(header, payload, signing_key) 28 | expect(Jws.verify jws, algorithm, nil).to include({error: 'invalid'}) 29 | end 30 | end 31 | 32 | describe 'w passing a changed key to #verify' do 33 | let(:changed_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9Z' } 34 | it 'returns error' do 35 | jws = Jws.sign(header, payload, signing_key) 36 | expect(Jws.verify jws, algorithm, changed_key).to include({error: 'invalid'}) 37 | end 38 | end 39 | end 40 | 41 | describe 'w/o passing a matching algorithm to #verify' do 42 | let(:algorithm) { 'RS256' } 43 | it 'raises' do 44 | jws = Jws.sign(header, payload, signing_key) 45 | expect { Jws.verify(jws, algorithm, verifying_key) } 46 | .to raise_error(RuntimeError, "Algorithm not matching 'alg' header parameter") 47 | end 48 | end 49 | end 50 | end 51 | 52 | context "w ES256 'alg' header parameter" do 53 | let(:header) { {alg: 'ES256'} } 54 | describe 'w passing a matching algorithm to #verify' do 55 | let(:algorithm) { 'ES256' } 56 | it 'w a jws' do 57 | private_key = EcdsaKey.curve_new('256') 58 | public_key_str = EcdsaKey.public_key_str(private_key) 59 | public_key = EcdsaKey.public_key_new('256', public_key_str) 60 | 61 | jws = Jws.sign(header, payload, private_key) 62 | expect(Jws.verify jws, algorithm, public_key).to include({ok: jws}) 63 | end 64 | end 65 | end 66 | end 67 | 68 | context 'header validation' do 69 | let(:signing_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' } 70 | describe "w/o a recognized 'alg' header parameter" do 71 | let(:header) { {alg: 'HS257'} } 72 | it 'raises' do 73 | expect { Jws.sign(header, payload, signing_key) } 74 | .to raise_error(RuntimeError, 'Unrecognized algorithm') 75 | end 76 | end 77 | 78 | describe "w/o a required 'alg' header parameter" do 79 | let(:header) { {typ: 'JWT'} } 80 | it 'raises' do 81 | expect { Jws.sign(header, payload, signing_key) } 82 | .to raise_error(RuntimeError, "Missing required 'alg' header parameter") 83 | end 84 | end 85 | end 86 | 87 | context '#unsecured_message' do 88 | context 'w valid header' do 89 | let(:header) { {alg: 'none'} } 90 | describe 'w passing a matching algorithm to #verify' do 91 | let(:algorithm) { 'none' } 92 | it 'w a jws' do 93 | jws = Jws.unsecured_message(header, payload) 94 | expect(Jws.verify jws, algorithm).to include({ok: jws}) 95 | end 96 | end 97 | 98 | describe 'w/o passing a matching algorithm to #verify' do 99 | let(:algorithm) { 'HS256' } 100 | let(:verifying_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' } 101 | it 'raises' do 102 | jws = Jws.unsecured_message(header, payload) 103 | expect { Jws.verify(jws, algorithm, verifying_key) } 104 | .to raise_error(RuntimeError, "Algorithm not matching 'alg' header parameter") 105 | end 106 | end 107 | end 108 | 109 | describe 'w invalid header' do 110 | let(:header) { {alg: 'HS256'} } 111 | it 'raises' do 112 | expect { Jws.unsecured_message(header, payload) } 113 | .to raise_error(RuntimeError, "Invalid 'alg' header parameter") 114 | end 115 | end 116 | end 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /spec/json_web_token/jwt_spec.rb: -------------------------------------------------------------------------------- 1 | require 'json_web_token/jwt' 2 | require 'support/ecdsa_key' 3 | require 'support/plausible_jwt' 4 | 5 | module JsonWebToken 6 | describe Jwt do 7 | context '#sign' do 8 | shared_examples_for 'does #verify' do 9 | it 'w a claims set' do 10 | jwt = Jwt.sign(claims, sign_options) 11 | expect(Jwt.verify(jwt, verify_options)[:ok]).to include(claims) 12 | end 13 | end 14 | 15 | shared_examples_for 'return a jwt' do 16 | it 'that is plausible' do 17 | jwt = Jwt.sign(claims, sign_options) 18 | expect(plausible_message_signature? jwt).to be true 19 | end 20 | end 21 | 22 | context 'w claims' do 23 | let(:claims) { { iss: 'joe', exp: 1300819380, :'http://example.com/is_root' => true} } 24 | context 'w HS256 keys' do 25 | let(:signing_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' } 26 | let(:verifying_key) { signing_key } 27 | let(:verify_options) { {key: verifying_key} } 28 | describe 'default header' do 29 | let(:sign_options) { {key: signing_key} } 30 | it_behaves_like 'does #verify' 31 | it_behaves_like 'return a jwt' 32 | end 33 | 34 | describe 'w alg option' do 35 | let(:sign_options) { {alg: 'HS256', key: signing_key} } 36 | it_behaves_like 'does #verify' 37 | it_behaves_like 'return a jwt' 38 | end 39 | 40 | describe 'w alg: nil option' do 41 | let(:sign_options) { {alg: nil, key: signing_key} } 42 | it_behaves_like 'does #verify' 43 | it_behaves_like 'return a jwt' 44 | end 45 | 46 | describe "w alg empty string option" do 47 | let(:sign_options) { {alg: '', key: signing_key} } 48 | it_behaves_like 'does #verify' 49 | it_behaves_like 'return a jwt' 50 | end 51 | 52 | describe "w alg: 'none' option" do 53 | let(:sign_options) { {alg: 'none', key: signing_key} } 54 | it 'raises' do 55 | jwt = Jwt.sign(claims, sign_options) 56 | expect { Jwt.verify(jwt, verify_options) } 57 | .to raise_error(RuntimeError, "Algorithm not matching 'alg' header parameter") 58 | end 59 | end 60 | end 61 | 62 | describe 'w/o key w default header alg' do 63 | it 'raises' do 64 | expect { Jwt.sign(claims, {}) } 65 | .to raise_error(RuntimeError, 'Invalid shared key') 66 | end 67 | end 68 | 69 | describe 'w HS256 key changed' do 70 | let(:sign_options) { {alg: 'HS256', key: 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C'} } 71 | let(:changed_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9Z' } 72 | let(:verify_options) { {key: verifying_key} } 73 | it 'raises' do 74 | jwt = Jwt.sign(claims, sign_options) 75 | expect(Jwt.verify jwt, {key: changed_key}).to include(error: 'invalid') 76 | end 77 | end 78 | 79 | context "w ES256 'alg' header parameter" do 80 | let(:algorithm) { 'ES256' } 81 | describe 'w passing a matching algorithm to #verify' do 82 | it 'is verified and plausible' do 83 | private_key = EcdsaKey.curve_new('256') 84 | public_key_str = EcdsaKey.public_key_str(private_key) 85 | public_key = EcdsaKey.public_key_new('256', public_key_str) 86 | 87 | sign_options = {alg: algorithm, key: private_key} 88 | jwt = Jwt.sign(claims, sign_options) 89 | 90 | verify_options = {alg: algorithm, key: public_key} 91 | expect(Jwt.verify(jwt, verify_options)[:ok]).to eql claims 92 | 93 | expect(plausible_message_signature? jwt, 64).to be true 94 | end 95 | end 96 | end 97 | 98 | context 'w/o key' do 99 | context "w alg: 'none' header parameter" do 100 | let(:sign_options) { {alg: 'none'} } 101 | describe "w verify alg: 'none'" do 102 | let(:verify_options) { {alg: 'none'} } 103 | it 'verifies a plausible unsecured jws' do 104 | jwt = Jwt.sign(claims, sign_options) 105 | expect(Jwt.verify(jwt, verify_options)[:ok]).to include(claims) 106 | expect(plausible_unsecured_message? jwt).to be true 107 | end 108 | end 109 | 110 | describe 'w default verify alg' do 111 | it 'raises' do 112 | jwt = Jwt.sign(claims, sign_options) 113 | expect { Jwt.verify(jwt, {alg: nil}) } 114 | .to raise_error(RuntimeError, "Algorithm not matching 'alg' header parameter") 115 | end 116 | end 117 | end 118 | end 119 | end 120 | 121 | context 'param validation' do 122 | let(:options) { {key: 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C'} } 123 | shared_examples_for 'w/o claims' do 124 | it 'raises' do 125 | expect { Jwt.sign(claims, options) } 126 | .to raise_error(RuntimeError, 'Claims blank') 127 | end 128 | end 129 | 130 | describe 'w claims nil' do 131 | let(:claims) { nil } 132 | it_behaves_like 'w/o claims' 133 | end 134 | 135 | describe 'w claims an empty string' do 136 | let(:claims) { '' } 137 | it_behaves_like 'w/o claims' 138 | end 139 | end 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /spec/json_web_token/util_spec.rb: -------------------------------------------------------------------------------- 1 | require 'json_web_token/util' 2 | 3 | module JsonWebToken 4 | describe Util do 5 | describe '#constant_time_compare?' do 6 | it 'guards against empty or nil strings' do 7 | expect(Util.constant_time_compare? 'a', 'a').to be true 8 | 9 | expect(Util.constant_time_compare? 'a', 'b').to be false 10 | expect(Util.constant_time_compare? 'a', 'A').to be false 11 | expect(Util.constant_time_compare? '', '').to be false 12 | expect(Util.constant_time_compare? nil, nil).to be false 13 | end 14 | end 15 | 16 | describe '#symbolize_keys' do 17 | it 'returns a new hash with all keys converted to symbols' do 18 | original = {'a' => 0, 'b' => '2', c: '3'} 19 | expect(Util.symbolize_keys original).to include({a: 0, b: '2', c: '3'}) 20 | expect(original).to eql original 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/json_web_token_spec.rb: -------------------------------------------------------------------------------- 1 | require 'json_web_token' 2 | 3 | describe JsonWebToken do 4 | context '#sign' do 5 | let(:claims) { { iss: 'joe', exp: 1300819380, :'http://example.com/is_root' => true} } 6 | shared_examples_for 'w #verify' do 7 | it 'w a claims set' do 8 | jwt = JsonWebToken.sign(claims, sign_options) 9 | expect(JsonWebToken.verify(jwt, verify_options)[:ok]).to include(claims) 10 | end 11 | end 12 | 13 | context 'w HS256 keys' do 14 | let(:signing_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' } 15 | let(:verifying_key) { signing_key } 16 | 17 | describe 'default alg' do 18 | let(:sign_options) { {key: signing_key} } 19 | let(:verify_options) { {key: verifying_key} } 20 | it_behaves_like 'w #verify' 21 | end 22 | 23 | context "w 'alg' option" do 24 | describe 'HS256' do 25 | let(:sign_options) { {alg: 'HS256', key: signing_key} } 26 | let(:verify_options) { {alg: 'HS256', key: verifying_key} } 27 | it_behaves_like 'w #verify' 28 | end 29 | 30 | describe "w alg 'none'" do 31 | let(:sign_options) { {alg: 'none', key: signing_key} } 32 | let(:verify_options) { {alg: 'none', key: verifying_key} } 33 | it_behaves_like 'w #verify' 34 | end 35 | end 36 | end 37 | end 38 | 39 | context 'module alias JWT' do 40 | describe '#sign' do 41 | let(:claims) { { iss: 'joe', exp: 1300819380, :'http://example.com/is_root' => true} } 42 | it 'recognized' do 43 | expect(JsonWebToken.sign(claims, key: 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C')).to be 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | 4 | # Conventionally, all specs live under a `spec` directory, which RSpec adds to 5 | # the `$LOAD_PATH`. The generated `.rspec` file contains `--require spec_helper` 6 | # which will cause this file to always be loaded, without a need to explicitly 7 | # require it in any files. 8 | # 9 | # Given that it is always loaded, you are encouraged to keep this file as 10 | # light-weight as possible. Requiring heavyweight dependencies from this file 11 | # will add to the boot time of your test suite on EVERY test run, even for an 12 | # individual file that may not need all of that loaded. Instead, consider 13 | # making a separate helper file that requires the additional dependencies and 14 | # performs the additional setup, and require it from the spec files that 15 | # actually need it. 16 | # 17 | # The `.rspec` file also contains a few flags that are not defaults but that 18 | # users commonly want. 19 | # 20 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 21 | RSpec.configure do |config| 22 | config.expect_with :rspec do |expectations| 23 | # This option will default to `true` in RSpec 4. It makes the `description` 24 | # and `failure_message` of custom matchers include text for helper methods 25 | # defined using `chain`, e.g.: 26 | # be_bigger_than(2).and_smaller_than(4).description 27 | # # => "be bigger than 2 and smaller than 4" 28 | # ...rather than: 29 | # # => "be bigger than 2" 30 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 31 | end 32 | 33 | config.mock_with :rspec do |mocks| 34 | # Prevents you from mocking or stubbing a method that does not exist on 35 | # a real object. This is generally recommended, and will default to 36 | # `true` in RSpec 4. 37 | mocks.verify_partial_doubles = true 38 | end 39 | 40 | # The settings below are suggested to provide a good initial experience 41 | # with RSpec, but feel free to customize to your heart's content. 42 | 43 | # These two settings work together to allow you to limit a spec run to 44 | # individual examples or groups you care about by tagging them with `:focus` 45 | # metadata. When nothing is tagged with `:focus`, all examples get run. 46 | config.filter_run :focus 47 | config.run_all_when_everything_filtered = true 48 | 49 | # Allows RSpec to persist some state between runs in order to support the 50 | # `--only-failures` and `--next-failure` CLI options. We recommend you 51 | # configure your source control system to ignore this file. 52 | config.example_status_persistence_file_path = 'spec/examples.txt' 53 | 54 | # Limits the available syntax to the non-monkey patched syntax that is 55 | # recommended. For more details, see: 56 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 57 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 58 | # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching 59 | # config.disable_monkey_patching! 60 | 61 | # Many RSpec users commonly either run the entire suite or an individual 62 | # file, and it's useful to allow more verbose output when running an 63 | # individual spec file. 64 | if config.files_to_run.one? 65 | # Use the documentation formatter for detailed output, unless a formatter 66 | # has already been configured (e.g. via a command-line flag) 67 | config.default_formatter = 'doc' 68 | end 69 | 70 | # Print the 10 slowest examples and example groups at the end of the spec 71 | # run, to help surface which specs are running particularly slowly. 72 | # config.profile_examples = 10 73 | 74 | # Run specs in random order to surface order dependencies. If you find an 75 | # order dependency and want to debug it, you can fix the order by providing 76 | # the seed, which is printed after each run. 77 | # --seed 1234 78 | config.order = :random 79 | 80 | # Seed global randomization in this process using the `--seed` CLI option. 81 | # Setting this allows you to use `--seed` to deterministically reproduce 82 | # test failures related to randomization by passing the same `--seed` value 83 | # as the one that triggered the failure. 84 | Kernel.srand config.seed 85 | end 86 | -------------------------------------------------------------------------------- /spec/support/ecdsa_key.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | 3 | module EcdsaKey 4 | 5 | BUILT_IN_CURVES = { 6 | '256' => 'prime256v1', 7 | '384' => 'secp384r1', 8 | '512' => 'secp521r1' 9 | } 10 | 11 | module_function 12 | 13 | def curve_new(sha_bits) 14 | OpenSSL::PKey::EC.new(BUILT_IN_CURVES[sha_bits]) 15 | end 16 | 17 | def public_key_str(curve, base = 16) 18 | curve.generate_key unless curve.private_key 19 | curve.public_key.to_bn.to_s(base) 20 | end 21 | 22 | def public_key_new(sha_bits, public_key_str, base = 16) 23 | curve_name = BUILT_IN_CURVES[sha_bits] 24 | fail('Unsupported curve') unless curve_name 25 | group = OpenSSL::PKey::EC::Group.new(curve_name) 26 | curve = OpenSSL::PKey::EC.new(group) 27 | curve.public_key = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(public_key_str, base)) 28 | curve 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/support/plausible_jwt.rb: -------------------------------------------------------------------------------- 1 | require 'json_web_token/format/base64_url' 2 | 3 | include JsonWebToken::Format::Base64Url 4 | 5 | def plausible_message_signature?(str, bytesize = 32) 6 | parts = str.split('.') 7 | return false unless parts.length == 3 8 | mac = decode(parts[2]) 9 | mac.bytesize == bytesize && mac.class == String 10 | end 11 | 12 | def plausible_unsecured_message?(str) 13 | return false unless str.end_with?('.') 14 | str.split('.').length == 2 15 | end 16 | --------------------------------------------------------------------------------