json_web_token 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []