json_web_token 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d9c63b5c2fa2302f1cee322ac44cb54eddda7538
4
+ data.tar.gz: c2215846a69b4d8f3a4705dbedbbebfbb32253ae
5
+ SHA512:
6
+ metadata.gz: a4eb15f1a8da6eded296e6165d1ed0ffe7d62b21f11116efc7108385f0202150511a0f87325fab3ec4b243318923759d241a607b3650126f83a5885a066c78df
7
+ data.tar.gz: 5be8f29d618889f7315230b94b1b0694eb6c80d78d0006aec65cea4835d783af66f23e13a7d5a5ede42971a4145a5c44e25c19aeee3340dd1f981fb5d60bc673
data/.gitignore ADDED
@@ -0,0 +1,35 @@
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
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'simplecov', '~> 0.10', require: false
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Gary Fleshman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # JSON Web Token
2
+
3
+ ## A JSON Web Token implementation for Ruby
4
+ **Work in progress -- not yet ready for production**
5
+
6
+ ### Description
7
+ A Ruby implementation of the JSON Web Token (JWT) Standards Track [RFC 7519][rfc7519]
8
+
9
+ ## Installation
10
+ gem install json_web_token
11
+
12
+ ### Philosophy & Design Goals
13
+ * Minimal API surface area
14
+ * Clear separation and conformance to underlying standards
15
+ - JSON Web Signature (JWS) Standards Track [RFC 7515][rfc7515]
16
+ - JSON Web Algorithms (JWA) Standards Track [RFC 7518][rfc7518]
17
+ * Thorough test coverage
18
+ * Modularity for comprehension and extensibility
19
+ * Implement only the REQUIRED elements of the JWT standard (initially)
20
+
21
+ ### Intended Audience
22
+ Token authentication of API requests to Rails via these popular gems
23
+
24
+ - [Devise][devise]
25
+ - [Doorkeeper][doorkeeper]
26
+ - [OAuth2][oauth2]
27
+
28
+ Secure Cross-Origin Resource Sharing ([CORS][cors]) using the [rack-cors][rack-cors] gem
29
+
30
+ ## Usage
31
+ Create a JSON web token
32
+
33
+ ```ruby
34
+ require 'json_web_token'
35
+
36
+ JsonWebToken.create(claims, options)
37
+ ```
38
+
39
+ Validate a JSON web token
40
+
41
+ ```ruby
42
+ claims = JsonWebToken.validate(jwt, options)
43
+ ```
44
+ ### Supported encryption algorithms
45
+ The 2 REQUIRED JWT algorithms
46
+
47
+ - HMAC using SHA-256 per [RFC 2104][rfc2104]
48
+ - 'none' (unsecured)
49
+
50
+ ### Supported Ruby Versions
51
+ Ruby 2.1 and up
52
+
53
+ ### Limitations
54
+ Future implementation may include these features:
55
+
56
+ - RECOMMENDED or OPTIONAL encryption algorithms
57
+ - Representation of a JWT as a JSON Web Encryption (JWE) [RFC 7516][rfc7516]
58
+ - OPTIONAL nested JWTs
59
+
60
+ [rfc2104]: http://tools.ietf.org/html/rfc2104
61
+ [rfc7515]: http://tools.ietf.org/html/rfc7515
62
+ [rfc7516]: http://tools.ietf.org/html/rfc7516
63
+ [rfc7518]: http://tools.ietf.org/html/rfc7518
64
+ [rfc7519]: http://tools.ietf.org/html/rfc7519
65
+
66
+ [cors]: http://www.w3.org/TR/cors/
67
+ [devise]: https://github.com/plataformatec/devise
68
+ [doorkeeper]: https://github.com/doorkeeper-gem/doorkeeper
69
+ [oauth2]: https://github.com/intridea/oauth2
70
+ [rack-cors]: https://github.com/cyu/rack-cors
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require 'json_web_token/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.author = 'Gary Fleshman'
7
+ s.email = 'gf4cl@verizon.net'
8
+ s.files = `git ls-files`.split("\n")
9
+ s.homepage = 'https://github.com/garyf/json_web_token'
10
+ s.name = 'json_web_token'
11
+ s.platform = Gem::Platform::RUBY
12
+ s.summary = 'JSON Web Token for Ruby'
13
+ s.version = JsonWebToken::VERSION
14
+ # recommended
15
+ s.license = 'MIT'
16
+ # optional
17
+ s.add_runtime_dependency 'json', '~> 1.8', '>= 1.8.3'
18
+ s.add_development_dependency 'pry-byebug', '~> 3.1'
19
+ s.add_development_dependency 'rspec', '~> 3.3'
20
+ s.description = 'Ruby implementation of the JSON Web Token Standard Track RFC 4627'
21
+ end
@@ -0,0 +1,14 @@
1
+ require 'json_web_token/jwt'
2
+
3
+ module JsonWebToken
4
+
5
+ module_function
6
+
7
+ def create(claims, options = {})
8
+ Jwt.create(claims, options)
9
+ end
10
+
11
+ def validate(jwt, options = {})
12
+ Jwt.validate(jwt, options)
13
+ end
14
+ end
@@ -0,0 +1,47 @@
1
+ require 'json_web_token/util'
2
+ require 'openssl'
3
+
4
+ module JsonWebToken
5
+ module Algorithm
6
+ module Hmac
7
+
8
+ SHA_BITS = [
9
+ '256',
10
+ '384',
11
+ '512'
12
+ ]
13
+
14
+ module_function
15
+
16
+ def signed(sha_bits, key, data)
17
+ validate_params(key, sha_bits)
18
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new("sha#{sha_bits}"), key, data)
19
+ end
20
+
21
+ def verified?(mac, sha_bits, key, data)
22
+ validate_params(key, sha_bits)
23
+ Util.constant_time_compare(mac, signed(sha_bits, key, data))
24
+ end
25
+
26
+ # private
27
+
28
+ def validate_params(key, sha_bits)
29
+ validate_sha_bits(sha_bits)
30
+ validate_key_size(key, sha_bits)
31
+ end
32
+
33
+ def validate_sha_bits(sha_bits)
34
+ fail('Invalid sha_bits') unless SHA_BITS.include?(sha_bits)
35
+ end
36
+
37
+ # http://tools.ietf.org/html/rfc7518#section-3.2
38
+ def validate_key_size(key, sha_bits)
39
+ fail('Invalid key') unless key && key.bytesize * 8 >= sha_bits.to_i
40
+ end
41
+
42
+ private_class_method :validate_params,
43
+ :validate_sha_bits,
44
+ :validate_key_size
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,45 @@
1
+ require 'base64'
2
+
3
+ module JsonWebToken
4
+ module Format
5
+ module Base64Url
6
+
7
+ module_function
8
+
9
+ def encode(str)
10
+ url_safe_encode(str)
11
+ end
12
+
13
+ def decode(str)
14
+ url_safe_decode(str)
15
+ end
16
+
17
+ # private
18
+
19
+ # http://tools.ietf.org/html/rfc7515#appendix-C
20
+ def url_safe_encode(str)
21
+ remove_base64_padding(Base64.urlsafe_encode64 str)
22
+ end
23
+
24
+ def url_safe_decode(str)
25
+ Base64.urlsafe_decode64(add_base64_padding str)
26
+ end
27
+
28
+ def remove_base64_padding(encoded)
29
+ encoded.gsub(/[=]/, '')
30
+ end
31
+
32
+ def add_base64_padding(str)
33
+ mod = str.length % 4
34
+ return str if mod == 0
35
+ fail('Invalid base64 string') if mod == 1
36
+ "#{str}#{'=' * (4 - mod)}"
37
+ end
38
+
39
+ private_class_method :url_safe_encode,
40
+ :url_safe_decode,
41
+ :remove_base64_padding,
42
+ :add_base64_padding
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,52 @@
1
+ require 'json_web_token/algorithm/hmac'
2
+
3
+ module JsonWebToken
4
+ module Jwa
5
+
6
+ ALGORITHMS = /(HS)(256|384|512)?/i
7
+ ALG_LENGTH = 5
8
+
9
+ module_function
10
+
11
+ def signed(algorithm, key, data)
12
+ alg = validated_alg(algorithm)
13
+ sha_bits = alg[:sha_bits]
14
+ case alg[:kind]
15
+ when 'hs'
16
+ Algorithm::Hmac.signed(sha_bits, key, data)
17
+ else
18
+ fail('Unsupported algorithm')
19
+ end
20
+ end
21
+
22
+ def verified?(signature, algorithm, key, data)
23
+ alg = validated_alg(algorithm)
24
+ sha_bits = alg[:sha_bits]
25
+ case alg[:kind]
26
+ when 'hs'
27
+ Algorithm::Hmac.verified?(signature, sha_bits, key, data)
28
+ else
29
+ false
30
+ end
31
+ end
32
+
33
+ # private
34
+
35
+ def validated_alg(algorithm)
36
+ alg = destructured_alg(algorithm)
37
+ alg ? alg : fail('Unrecognized algorithm')
38
+ end
39
+
40
+ def destructured_alg(algorithm)
41
+ match = algorithm.match(ALGORITHMS)
42
+ return unless match && match[0].length == ALG_LENGTH
43
+ {
44
+ kind: match[1].downcase,
45
+ sha_bits: match[2]
46
+ }
47
+ end
48
+
49
+ private_class_method :validated_alg,
50
+ :destructured_alg
51
+ end
52
+ end
@@ -0,0 +1,75 @@
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
+ module Jws
8
+
9
+ MESSAGE_SIGNATURE_PARTS = 3
10
+
11
+ module_function
12
+
13
+ # http://tools.ietf.org/html/rfc7515#page-15
14
+ def message_signature(header, payload, key)
15
+ alg = alg_parameter(header)
16
+ data = signing_input(header, payload)
17
+ "#{data}.#{signature(alg, key, data)}"
18
+ end
19
+
20
+ # http://tools.ietf.org/html/rfc7515#page-16
21
+ def validate(jws, algorithm, key = nil)
22
+ compare_alg(jws, algorithm)
23
+ return jws if algorithm == 'none'
24
+ signature_valid?(jws, algorithm, key) ? jws : 'Invalid'
25
+ end
26
+
27
+ # http://tools.ietf.org/html/rfc7515#page-47
28
+ def unsecured_jws(header, payload)
29
+ fail("Invalid 'alg' header parameter") unless alg_parameter(header) == 'none'
30
+ "#{signing_input(header, payload)}." # note trailing '.'
31
+ end
32
+
33
+ # private
34
+
35
+ def alg_parameter(header)
36
+ alg = Util.symbolize_keys(header)[:alg]
37
+ alg && !alg.empty? ? alg : fail("Missing required 'alg' header parameter")
38
+ end
39
+
40
+ def signing_input(header, payload)
41
+ "#{Format::Base64Url.encode header.to_json}.#{Format::Base64Url.encode payload}"
42
+ end
43
+
44
+ def signature(algorithm, key, data)
45
+ Format::Base64Url.encode(Jwa.signed algorithm, key, data)
46
+ end
47
+
48
+ # http://tools.ietf.org/html/rfc7515#section-4.1.1
49
+ def compare_alg(jws, algorithm)
50
+ header = decoded_header_json_to_hash(jws)
51
+ unless alg_parameter(header) == algorithm
52
+ fail("Algorithm not matching 'alg' header parameter")
53
+ end
54
+ end
55
+
56
+ def decoded_header_json_to_hash(jws)
57
+ JSON.parse(Format::Base64Url.decode jws.split('.')[0])
58
+ end
59
+
60
+ def signature_valid?(jws, algorithm, key)
61
+ ary = jws.split('.')
62
+ return unless key && ary.length == MESSAGE_SIGNATURE_PARTS
63
+ decoded_signature = Format::Base64Url.decode(ary[2])
64
+ payload = "#{ary[0]}.#{ary[1]}"
65
+ Jwa.verified?(decoded_signature, algorithm, key, payload)
66
+ end
67
+
68
+ private_class_method :alg_parameter,
69
+ :signing_input,
70
+ :signature,
71
+ :compare_alg,
72
+ :decoded_header_json_to_hash,
73
+ :signature_valid?
74
+ end
75
+ end
@@ -0,0 +1,57 @@
1
+ require 'json_web_token/jws'
2
+
3
+ module JsonWebToken
4
+ module Jwt
5
+
6
+ ALGORITHM_DEFAULT = 'HS256'
7
+ HEADER_DEFAULT = {
8
+ typ: 'JWT',
9
+ alg: ALGORITHM_DEFAULT
10
+ }
11
+
12
+ module_function
13
+
14
+ # http://tools.ietf.org/html/rfc7519#page-12
15
+ def create(claims, options = {})
16
+ message = validated_message(claims)
17
+ key = options[:key]
18
+ header = config_header(options)
19
+ return Jws.unsecured_jws(header, message) if header[:alg] == 'none'
20
+ Jws.message_signature(header, message, key)
21
+ end
22
+
23
+ def validate(jwt, options = {})
24
+ alg = options[:alg] || ALGORITHM_DEFAULT
25
+ jws = Jws.validate(jwt, alg, options[:key])
26
+ jws == 'Invalid' ? jws : Util.symbolize_keys(decoded_message_json_to_hash jws)
27
+ end
28
+
29
+ # private
30
+
31
+ def validated_message(claims)
32
+ fail('Claims not provided') if !claims || claims.empty?
33
+ claims.to_json
34
+ end
35
+
36
+ def config_header(options)
37
+ HEADER_DEFAULT.merge(alg_parameter_required options)
38
+ end
39
+
40
+ def alg_parameter_required(options)
41
+ hsh = options.select { |k, _v| k == :alg } # filter unsupported keys
42
+ alg = hsh[:alg]
43
+ alg && !alg.empty? ? hsh : {}
44
+ end
45
+
46
+ def decoded_message_json_to_hash(jws)
47
+ ary = jws.split('.')
48
+ return jws unless ary.length > 1 # invalid
49
+ JSON.parse(Format::Base64Url.decode ary[1])
50
+ end
51
+
52
+ private_class_method :validated_message,
53
+ :config_header,
54
+ :alg_parameter_required,
55
+ :decoded_message_json_to_hash
56
+ end
57
+ end
@@ -0,0 +1,37 @@
1
+ module JsonWebToken
2
+ module Util
3
+
4
+ module_function
5
+
6
+ # https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#section-3.2
7
+ def constant_time_compare(a, b)
8
+ return false if a.nil? || b.nil? || a.empty? || b.empty?
9
+ secure_compare(a, b)
10
+ end
11
+
12
+ # cf. rails activesupport/lib/active_support/core_ext/hash/keys.rb
13
+ def symbolize_keys(hsh)
14
+ transform_keys(hsh) { |key| key.to_sym rescue key }
15
+ end
16
+
17
+ # private
18
+
19
+ # cf. rails activesupport/lib/active_support/security_utils.rb
20
+ def secure_compare(a, b)
21
+ return false unless a.bytesize == b.bytesize
22
+ l = a.unpack "C#{a.bytesize}"
23
+ res = 0
24
+ b.each_byte { |byte| res |= byte ^ l.shift }
25
+ res == 0
26
+ end
27
+
28
+ def transform_keys(hsh)
29
+ result = Hash.new
30
+ hsh.keys.each { |k| result[yield(k)] = hsh[k] }
31
+ result
32
+ end
33
+
34
+ private_class_method :secure_compare,
35
+ :transform_keys
36
+ end
37
+ end
@@ -0,0 +1,3 @@
1
+ module JsonWebToken
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,131 @@
1
+ require 'json_web_token/algorithm/hmac'
2
+
3
+ module JsonWebToken
4
+ module Algorithm
5
+ describe Hmac do
6
+ context 'detect changed signing_input or MAC' do
7
+ let(:signing_input) { 'signing_input' }
8
+ let(:changed_signing_input) { 'changed_signing_input' }
9
+ shared_examples_for '#signed' do
10
+ it 'is #verified?' do
11
+ mac = Hmac.signed(sha_bits, key, signing_input)
12
+ expect(Hmac.verified? mac, sha_bits, key, signing_input).to be true
13
+ expect(Hmac.verified? mac, sha_bits, key, changed_signing_input).to be false
14
+
15
+ changed_mac = Hmac.signed(sha_bits, key, changed_signing_input)
16
+ expect(Hmac.verified? changed_mac, sha_bits, key, signing_input).to be false
17
+ end
18
+ end
19
+
20
+ describe 'HS256' do
21
+ let(:sha_bits) { '256' }
22
+ let(:key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' }
23
+ it_behaves_like '#signed'
24
+ end
25
+
26
+ describe 'HS384' do
27
+ let(:sha_bits) { '384' }
28
+ let(:key) { 'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS' }
29
+ it_behaves_like '#signed'
30
+ end
31
+
32
+ describe 'HS512' do
33
+ let(:sha_bits) { '512' }
34
+ let(:key) { 'ysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hc' }
35
+ it_behaves_like '#signed'
36
+ end
37
+ end
38
+
39
+ describe 'changed key' do
40
+ let(:sha_bits) { '256' }
41
+ let(:key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' }
42
+ let(:changed_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9Z' }
43
+ let(:data) { 'data' }
44
+ it 'fails #verified?' do
45
+ mac = Hmac.signed(sha_bits, key, data)
46
+ expect(Hmac.verified? mac, sha_bits, key, data).to be true
47
+ expect(Hmac.verified? mac, sha_bits, changed_key, data).to be false
48
+ end
49
+ end
50
+
51
+ context 'param validation' do
52
+ let(:data) { 'data' }
53
+ shared_examples_for 'invalid key' do
54
+ it 'raises' do
55
+ expect { Hmac.signed(sha_bits, key, data) }.to raise_error(RuntimeError, 'Invalid key')
56
+ end
57
+ end
58
+
59
+ context 'w 256 sha_bits' do
60
+ let(:sha_bits) { '256' }
61
+ describe 'key nil' do
62
+ let(:key) { nil }
63
+ it_behaves_like 'invalid key'
64
+ end
65
+
66
+ describe "key 'empty string'" do
67
+ let(:key) { '' }
68
+ it_behaves_like 'invalid key'
69
+ end
70
+
71
+ describe 'key length (31) < MAC length (32)' do
72
+ let(:key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9' }
73
+ it_behaves_like 'invalid key'
74
+ end
75
+
76
+ describe 'key length (32) == MAC length (32)' do
77
+ let(:key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' }
78
+ it 'returns a 32-byte MAC string' do
79
+ mac = Hmac.signed(sha_bits, key, data)
80
+ expect(mac.bytesize).to eql 32
81
+ expect(mac.class).to eql String
82
+ end
83
+ end
84
+ end
85
+
86
+ context 'w 384 sha_bits' do
87
+ let(:sha_bits) { '384' }
88
+ describe 'key length (47) < MAC length (48)' do
89
+ let(:key) { 'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1q' }
90
+ it_behaves_like 'invalid key'
91
+ end
92
+
93
+ describe 'key length (48) == MAC length (48)' do
94
+ let(:key) { 'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS' }
95
+ it 'returns a 48-byte MAC string' do
96
+ mac = Hmac.signed(sha_bits, key, data)
97
+ expect(mac.bytesize).to eql 48
98
+ expect(mac.class).to eql String
99
+ end
100
+ end
101
+ end
102
+
103
+ context 'w 512 sha_bits' do
104
+ let(:sha_bits) { '512' }
105
+ describe 'key length (63) < MAC length (64)' do
106
+ let(:key) { 'ysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4h' }
107
+ it_behaves_like 'invalid key'
108
+ end
109
+
110
+ describe 'key length (64) == MAC length (64)' do
111
+ let(:key) { 'ysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hc' }
112
+ it 'returns a 64-byte MAC string' do
113
+ mac = Hmac.signed(sha_bits, key, data)
114
+ expect(mac.bytesize).to eql 64
115
+ expect(mac.class).to eql String
116
+ end
117
+ end
118
+ end
119
+
120
+ describe 'w unrecognized sha_bits' do
121
+ let(:sha_bits) { '257' }
122
+ let(:key) { 'ysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hc' }
123
+ it 'raises' do
124
+ expect { Hmac.signed(sha_bits, key, data) }
125
+ .to raise_error(RuntimeError, 'Invalid sha_bits')
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,84 @@
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
@@ -0,0 +1,52 @@
1
+ require 'json_web_token/jwa'
2
+
3
+ module JsonWebToken
4
+ describe Jwa do
5
+ context 'detect changed signing_input or MAC' do
6
+ let(:signing_input) { 'signing_input' }
7
+ let(:changed_signing_input) { 'changed_signing_input' }
8
+ shared_examples_for '#signed' do
9
+ it 'is #verified?' do
10
+ mac = Jwa.signed(algorithm, signing_key, signing_input)
11
+ expect(Jwa.verified? mac, algorithm, verifying_key, signing_input).to be true
12
+ expect(Jwa.verified? mac, algorithm, verifying_key, changed_signing_input).to be false
13
+
14
+ changed_mac = Jwa.signed(algorithm, signing_key, changed_signing_input)
15
+ expect(Jwa.verified? changed_mac, algorithm, verifying_key, signing_input).to be false
16
+ end
17
+ end
18
+
19
+ describe 'HS256' do
20
+ let(:algorithm) { 'HS256' }
21
+ let(:signing_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' }
22
+ let(:verifying_key) { signing_key }
23
+ it_behaves_like '#signed'
24
+ end
25
+ end
26
+
27
+ context 'param validation' do
28
+ let(:data) { 'data' }
29
+ context 'w HS256 key' do
30
+ let(:key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' }
31
+ describe 'unrecognized algorithm' do
32
+ ['HT256', 'HS257', '', nil].each do |elt|
33
+ let(:algorithm) { "#{elt}" }
34
+ it 'raises' do
35
+ expect { Jwa.signed(algorithm, key, data) }
36
+ .to raise_error(RuntimeError, 'Unrecognized algorithm')
37
+ end
38
+ end
39
+ end
40
+
41
+ describe 'HS256' do
42
+ let(:algorithm) { 'HS256' }
43
+ it 'returns a 32-byte MAC string' do
44
+ mac = Jwa.signed(algorithm, key, data)
45
+ expect(mac.bytesize).to eql 32
46
+ expect(mac.class).to eql String
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,95 @@
1
+ require 'json_web_token/jws'
2
+
3
+ module JsonWebToken
4
+ describe Jws do
5
+ context 'w payload' do
6
+ let(:payload) { 'payload' }
7
+ context '#message_signature' do
8
+ shared_examples_for 'w #validate' do
9
+ it 'verified' do
10
+ jws = Jws.message_signature(header, payload, signing_key)
11
+ expect(Jws.validate jws, algorithm, verifying_key).to eql jws
12
+ end
13
+ end
14
+
15
+ context 'w HS256 keys' do
16
+ let(:signing_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' }
17
+ let(:verifying_key) { signing_key }
18
+ context "w HS256 'alg' header parameter" do
19
+ let(:header) { {alg: 'HS256'} }
20
+ describe 'w passing a matching algorithm to #validate' do
21
+ let(:algorithm) { 'HS256' }
22
+ it_behaves_like 'w #validate'
23
+
24
+ describe 'w/o passing key to #validate' do
25
+ it "returns 'Invalid'" do
26
+ jws = Jws.message_signature(header, payload, signing_key)
27
+ expect(Jws.validate jws, algorithm, nil).to eql 'Invalid'
28
+ end
29
+ end
30
+ end
31
+
32
+ describe 'w/o passing a matching algorithm to #validate' do
33
+ let(:algorithm) { 'RS256' }
34
+ it 'raises' do
35
+ jws = Jws.message_signature(header, payload, signing_key)
36
+ expect { Jws.validate(jws, algorithm, verifying_key) }
37
+ .to raise_error(RuntimeError, "Algorithm not matching 'alg' header parameter")
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ context 'header validation' do
45
+ let(:signing_key) { 'signing_key' }
46
+ describe "w/o a recognized 'alg' header parameter" do
47
+ let(:header) { {alg: 'HS257'} }
48
+ it 'raises' do
49
+ expect { Jws.message_signature(header, payload, signing_key) }
50
+ .to raise_error(RuntimeError, 'Unrecognized algorithm')
51
+ end
52
+ end
53
+
54
+ describe "w/o a required 'alg' header parameter" do
55
+ let(:header) { {typ: 'JWT'} }
56
+ it 'raises' do
57
+ expect { Jws.message_signature(header, payload, signing_key) }
58
+ .to raise_error(RuntimeError, "Missing required 'alg' header parameter")
59
+ end
60
+ end
61
+ end
62
+
63
+ context '#unsecured_jws' do
64
+ context 'w valid header' do
65
+ let(:header) { {alg: 'none'} }
66
+ describe 'w passing a matching algorithm to #validate' do
67
+ let(:algorithm) { 'none' }
68
+ it 'is verified' do
69
+ jws = Jws.unsecured_jws(header, payload)
70
+ expect(Jws.validate jws, algorithm).to eql jws
71
+ end
72
+ end
73
+
74
+ describe 'w/o passing a matching algorithm to #validate' do
75
+ let(:algorithm) { 'HS256' }
76
+ let(:verifying_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' }
77
+ it 'raises' do
78
+ jws = Jws.unsecured_jws(header, payload)
79
+ expect { Jws.validate(jws, algorithm, verifying_key) }
80
+ .to raise_error(RuntimeError, "Algorithm not matching 'alg' header parameter")
81
+ end
82
+ end
83
+ end
84
+
85
+ describe 'w invalid header' do
86
+ let(:header) { {alg: 'HS256'} }
87
+ it 'raises' do
88
+ expect { Jws.unsecured_jws(header, payload) }
89
+ .to raise_error(RuntimeError, "Invalid 'alg' header parameter")
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,93 @@
1
+ require 'json_web_token/jwt'
2
+ require 'support/plausible_jwt'
3
+
4
+ module JsonWebToken
5
+ describe Jwt do
6
+ context '#create' do
7
+ shared_examples_for 'w #validate' do
8
+ it 'verified' do
9
+ jwt = Jwt.create(claims, options)
10
+ expect(Jwt.validate jwt, options).to include(claims)
11
+ end
12
+ end
13
+
14
+ shared_examples_for 'return message signature' do
15
+ it 'plausible' do
16
+ serialized_output = Jwt.create(claims, options)
17
+ expect(plausible_message_signature? serialized_output).to be true
18
+ end
19
+ end
20
+
21
+ shared_examples_for 'return unsecured jws' do
22
+ it 'plausible' do
23
+ serialized_output = Jwt.create(claims, options)
24
+ expect(plausible_unsecured_jws? serialized_output).to be true
25
+ end
26
+ end
27
+
28
+ context 'w claims' do
29
+ let(:claims) { {exp: 'tomorrow'} }
30
+ context 'w key' do
31
+ let(:key) { 'this_a_32_character_private_key!' }
32
+ describe 'default header' do
33
+ let(:options) { {key: key} }
34
+ it_behaves_like 'w #validate'
35
+ it_behaves_like 'return message signature'
36
+ end
37
+
38
+ describe 'passing header parameters' do
39
+ let(:options) { {typ: 'JWT', alg: 'HS256', key: key} }
40
+ it_behaves_like 'w #validate'
41
+ it_behaves_like 'return message signature'
42
+ end
43
+
44
+ describe "w 'alg':'none' header parameter" do
45
+ let(:options) { {typ: 'JWT', alg: 'none', key: key} }
46
+ it_behaves_like 'w #validate'
47
+ it_behaves_like 'return unsecured jws'
48
+ end
49
+
50
+ describe "w 'alg':'nil' header parameter" do
51
+ let(:options) { {alg: nil, key: key} }
52
+ it_behaves_like 'w #validate'
53
+ it_behaves_like 'return message signature'
54
+ end
55
+
56
+ describe "w 'alg':'' header parameter" do
57
+ let(:options) { {alg: nil, key: key} }
58
+ it_behaves_like 'w #validate'
59
+ it_behaves_like 'return message signature'
60
+ end
61
+ end
62
+
63
+ context 'w/o key' do
64
+ let(:options) { {typ: 'JWT', alg: 'none'} }
65
+ describe "w 'alg':'none' header parameter" do
66
+ it_behaves_like 'w #validate'
67
+ it_behaves_like 'return unsecured jws'
68
+ end
69
+ end
70
+ end
71
+
72
+ shared_examples_for 'claims not provided' do
73
+ it 'raises' do
74
+ expect { Jwt.create(claims, options) }
75
+ .to raise_error(RuntimeError, 'Claims not provided')
76
+ end
77
+ end
78
+
79
+ context 'w secret' do
80
+ let(:options) { {key: 'secret'} }
81
+ describe 'w claims nil' do
82
+ let(:claims) { nil }
83
+ it_behaves_like 'claims not provided'
84
+ end
85
+
86
+ describe "w claims ''" do
87
+ let(:claims) { '' }
88
+ it_behaves_like 'claims not provided'
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,62 @@
1
+ require 'json_web_token'
2
+
3
+ describe JsonWebToken do
4
+ context '#create' do
5
+ let(:claims) { {exp: 'tomorrow'} }
6
+ shared_examples_for 'w #validate' do
7
+ it 'is verified' do
8
+ jwt = JsonWebToken.create(claims, create_options)
9
+ expect(JsonWebToken.validate jwt, validate_options).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(:create_options) { {key: signing_key} }
19
+ let(:validate_options) { {key: verifying_key} }
20
+ it_behaves_like 'w #validate'
21
+ end
22
+
23
+ context "w 'alg' option" do
24
+ describe 'HS256' do
25
+ let(:create_options) { {alg: 'HS256', key: signing_key} }
26
+ let(:validate_options) { {alg: 'HS256', key: verifying_key} }
27
+ it_behaves_like 'w #validate'
28
+ end
29
+
30
+ describe "w alg 'none'" do
31
+ let(:create_options) { {alg: 'none', key: signing_key} }
32
+ let(:validate_options) { {alg: 'none', key: verifying_key} }
33
+ it_behaves_like 'w #validate'
34
+ end
35
+ end
36
+ end
37
+
38
+ context 'w/o key' do
39
+ context "w create alg 'none'" do
40
+ let(:create_options) { {alg: 'none'} }
41
+ describe "w validate alg 'none'" do
42
+ let(:validate_options) { {alg: 'none'} }
43
+ it_behaves_like 'w #validate'
44
+ end
45
+
46
+ describe "w default validate alg" do
47
+ it 'raises' do
48
+ jwt = JsonWebToken.create(claims, create_options)
49
+ expect { JsonWebToken.validate(jwt) }
50
+ .to raise_error(RuntimeError, "Algorithm not matching 'alg' header parameter")
51
+ end
52
+ end
53
+ end
54
+
55
+ describe 'w default create alg' do
56
+ it 'raises' do
57
+ expect { JsonWebToken.create(claims) }.to raise_error(RuntimeError, 'Invalid key')
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,85 @@
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
@@ -0,0 +1,11 @@
1
+ def plausible_message_signature?(str)
2
+ _parts_count(str) == 3
3
+ end
4
+
5
+ def plausible_unsecured_jws?(str)
6
+ _parts_count(str) == 2
7
+ end
8
+
9
+ def _parts_count(str)
10
+ str.split('.').length
11
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: json_web_token
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Gary Fleshman
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-07-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.8'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.8.3
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.8'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.8.3
33
+ - !ruby/object:Gem::Dependency
34
+ name: pry-byebug
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.1'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.1'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.3'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.3'
61
+ description: Ruby implementation of the JSON Web Token Standard Track RFC 4627
62
+ email: gf4cl@verizon.net
63
+ executables: []
64
+ extensions: []
65
+ extra_rdoc_files: []
66
+ files:
67
+ - ".gitignore"
68
+ - ".rspec"
69
+ - Gemfile
70
+ - LICENSE
71
+ - README.md
72
+ - json_web_token.gemspec
73
+ - lib/json_web_token.rb
74
+ - lib/json_web_token/algorithm/hmac.rb
75
+ - lib/json_web_token/format/base64_url.rb
76
+ - lib/json_web_token/jwa.rb
77
+ - lib/json_web_token/jws.rb
78
+ - lib/json_web_token/jwt.rb
79
+ - lib/json_web_token/util.rb
80
+ - lib/json_web_token/version.rb
81
+ - spec/json_web_token/algorithm/hmac_spec.rb
82
+ - spec/json_web_token/format/base64_url_spec.rb
83
+ - spec/json_web_token/jwa_spec.rb
84
+ - spec/json_web_token/jws_spec.rb
85
+ - spec/json_web_token/jwt_spec.rb
86
+ - spec/json_web_token/util_spec.rb
87
+ - spec/json_web_token_spec.rb
88
+ - spec/spec_helper.rb
89
+ - spec/support/plausible_jwt.rb
90
+ homepage: https://github.com/garyf/json_web_token
91
+ licenses:
92
+ - MIT
93
+ metadata: {}
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubyforge_project:
110
+ rubygems_version: 2.4.8
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: JSON Web Token for Ruby
114
+ test_files: []