jwt 0.1.13 → 1.0.0
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.
- data/Manifest +1 -0
- data/Rakefile +1 -2
- data/jwt.gemspec +4 -7
- data/lib/jwt.rb +66 -36
- data/lib/jwt/json.rb +30 -0
- data/spec/jwt_spec.rb +46 -26
- metadata +4 -18
data/Manifest
CHANGED
data/Rakefile
CHANGED
@@ -2,13 +2,12 @@ require 'rubygems'
|
|
2
2
|
require 'rake'
|
3
3
|
require 'echoe'
|
4
4
|
|
5
|
-
Echoe.new('jwt', '0.
|
5
|
+
Echoe.new('jwt', '1.0.0') do |p|
|
6
6
|
p.description = "JSON Web Token implementation in Ruby"
|
7
7
|
p.url = "http://github.com/progrium/ruby-jwt"
|
8
8
|
p.author = "Jeff Lindsay"
|
9
9
|
p.email = "progrium@gmail.com"
|
10
10
|
p.ignore_pattern = ["tmp/*"]
|
11
|
-
p.runtime_dependencies = ["multi_json >=1.5"]
|
12
11
|
p.development_dependencies = ["echoe >=4.6.3"]
|
13
12
|
p.licenses = "MIT"
|
14
13
|
end
|
data/jwt.gemspec
CHANGED
@@ -2,15 +2,15 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = "jwt"
|
5
|
-
s.version = "0.
|
5
|
+
s.version = "1.0.0"
|
6
6
|
|
7
7
|
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
8
|
s.authors = ["Jeff Lindsay"]
|
9
|
-
s.date = "2014-05-
|
9
|
+
s.date = "2014-05-07"
|
10
10
|
s.description = "JSON Web Token implementation in Ruby"
|
11
11
|
s.email = "progrium@gmail.com"
|
12
|
-
s.extra_rdoc_files = ["lib/jwt.rb"]
|
13
|
-
s.files = ["Rakefile", "lib/jwt.rb", "spec/helper.rb", "spec/jwt_spec.rb", "Manifest", "jwt.gemspec"]
|
12
|
+
s.extra_rdoc_files = ["lib/jwt.rb", "lib/jwt/json.rb"]
|
13
|
+
s.files = ["Rakefile", "lib/jwt.rb", "lib/jwt/json.rb", "spec/helper.rb", "spec/jwt_spec.rb", "Manifest", "jwt.gemspec"]
|
14
14
|
s.homepage = "http://github.com/progrium/ruby-jwt"
|
15
15
|
s.licenses = ["MIT"]
|
16
16
|
s.rdoc_options = ["--line-numbers", "--title", "Jwt", "--main", "README.md"]
|
@@ -23,14 +23,11 @@ Gem::Specification.new do |s|
|
|
23
23
|
s.specification_version = 3
|
24
24
|
|
25
25
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
26
|
-
s.add_runtime_dependency(%q<multi_json>, [">= 1.5"])
|
27
26
|
s.add_development_dependency(%q<echoe>, [">= 4.6.3"])
|
28
27
|
else
|
29
|
-
s.add_dependency(%q<multi_json>, [">= 1.5"])
|
30
28
|
s.add_dependency(%q<echoe>, [">= 4.6.3"])
|
31
29
|
end
|
32
30
|
else
|
33
|
-
s.add_dependency(%q<multi_json>, [">= 1.5"])
|
34
31
|
s.add_dependency(%q<echoe>, [">= 4.6.3"])
|
35
32
|
end
|
36
33
|
end
|
data/lib/jwt.rb
CHANGED
@@ -6,10 +6,11 @@
|
|
6
6
|
|
7
7
|
require "base64"
|
8
8
|
require "openssl"
|
9
|
-
require "
|
9
|
+
require "jwt/json"
|
10
10
|
|
11
11
|
module JWT
|
12
12
|
class DecodeError < StandardError; end
|
13
|
+
extend JWT::Json
|
13
14
|
|
14
15
|
module_function
|
15
16
|
|
@@ -44,59 +45,88 @@ module JWT
|
|
44
45
|
Base64.encode64(str).tr("+/", "-_").gsub(/[\n=]/, "")
|
45
46
|
end
|
46
47
|
|
47
|
-
def
|
48
|
-
algorithm ||= "none"
|
49
|
-
segments = []
|
48
|
+
def encoded_header(algorithm="HS256", header_fields={})
|
50
49
|
header = {"typ" => "JWT", "alg" => algorithm}.merge(header_fields)
|
51
|
-
|
52
|
-
|
53
|
-
|
50
|
+
base64url_encode(encode_json(header))
|
51
|
+
end
|
52
|
+
|
53
|
+
def encoded_payload(payload)
|
54
|
+
base64url_encode(encode_json(payload))
|
55
|
+
end
|
56
|
+
|
57
|
+
def encoded_signature(signing_input, key, algorithm)
|
54
58
|
if algorithm == "none"
|
55
|
-
|
59
|
+
""
|
56
60
|
else
|
57
61
|
signature = sign(algorithm, signing_input, key)
|
58
|
-
|
62
|
+
base64url_encode(signature)
|
59
63
|
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def encode(payload, key, algorithm="HS256", header_fields={})
|
67
|
+
algorithm ||= "none"
|
68
|
+
segments = []
|
69
|
+
segments << encoded_header(algorithm, header_fields)
|
70
|
+
segments << encoded_payload(payload)
|
71
|
+
segments << encoded_signature(segments.join("."), key, algorithm)
|
60
72
|
segments.join(".")
|
61
73
|
end
|
62
74
|
|
63
|
-
def
|
75
|
+
def raw_segments(jwt, verify=true)
|
64
76
|
segments = jwt.split(".")
|
65
|
-
|
66
|
-
|
77
|
+
required_num_segments = verify ? [3] : [2,3]
|
78
|
+
raise JWT::DecodeError.new("Not enough or too many segments") unless required_num_segments.include? segments.length
|
79
|
+
segments
|
80
|
+
end
|
81
|
+
|
82
|
+
def decode_header_and_payload(header_segment, payload_segment)
|
83
|
+
header = decode_json(base64url_decode(header_segment))
|
84
|
+
payload = decode_json(base64url_decode(payload_segment))
|
85
|
+
[header, payload]
|
86
|
+
end
|
87
|
+
|
88
|
+
def decoded_segments(jwt, verify=true)
|
89
|
+
header_segment, payload_segment, crypto_segment = raw_segments(jwt, verify)
|
90
|
+
header, payload = decode_header_and_payload(header_segment, payload_segment)
|
91
|
+
signature = base64url_decode(crypto_segment.to_s) if verify
|
67
92
|
signing_input = [header_segment, payload_segment].join(".")
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
raise JWT::DecodeError.new("Invalid segment encoding")
|
74
|
-
end
|
93
|
+
[header, payload, signature, signing_input]
|
94
|
+
end
|
95
|
+
|
96
|
+
def decode(jwt, key=nil, verify=true, &keyfinder)
|
97
|
+
raise JWT::DecodeError.new("Nil JSON web token") unless jwt
|
75
98
|
|
99
|
+
header, payload, signature, signing_input = decoded_segments(jwt, verify)
|
76
100
|
raise JWT::DecodeError.new("Not enough or too many segments") unless header && payload
|
77
101
|
|
78
102
|
if verify
|
79
|
-
algo = header
|
103
|
+
algo, key = signature_algorithm_and_key(header, key, &keyfinder)
|
104
|
+
verify_signature(algo, key, signing_input, signature)
|
105
|
+
end
|
106
|
+
return payload,header
|
107
|
+
end
|
80
108
|
|
81
|
-
|
82
|
-
|
83
|
-
|
109
|
+
def signature_algorithm_and_key(header, key, &keyfinder)
|
110
|
+
if keyfinder
|
111
|
+
key = keyfinder.call(header)
|
112
|
+
end
|
113
|
+
[header['alg'], key]
|
114
|
+
end
|
84
115
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
rescue OpenSSL::PKey::PKeyError
|
94
|
-
raise JWT::DecodeError.new("Signature verification failed")
|
95
|
-
ensure
|
96
|
-
OpenSSL.errors.clear
|
116
|
+
def verify_signature(algo, key, signing_input, signature)
|
117
|
+
begin
|
118
|
+
if ["HS256", "HS384", "HS512"].include?(algo)
|
119
|
+
raise JWT::DecodeError.new("Signature verification failed") unless secure_compare(signature, sign_hmac(algo, signing_input, key))
|
120
|
+
elsif ["RS256", "RS384", "RS512"].include?(algo)
|
121
|
+
raise JWT::DecodeError.new("Signature verification failed") unless verify_rsa(algo, key, signing_input, signature)
|
122
|
+
else
|
123
|
+
raise JWT::DecodeError.new("Algorithm not supported")
|
97
124
|
end
|
125
|
+
rescue OpenSSL::PKey::PKeyError
|
126
|
+
raise JWT::DecodeError.new("Signature verification failed")
|
127
|
+
ensure
|
128
|
+
OpenSSL.errors.clear
|
98
129
|
end
|
99
|
-
payload
|
100
130
|
end
|
101
131
|
|
102
132
|
# From devise
|
data/lib/jwt/json.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module JWT
|
2
|
+
module Json
|
3
|
+
if RUBY_VERSION >= "1.9" && !defined?(MultiJson)
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
def decode_json(encoded)
|
7
|
+
JSON.parse(encoded)
|
8
|
+
rescue JSON::ParserError
|
9
|
+
raise JWT::DecodeError.new("Invalid segment encoding")
|
10
|
+
end
|
11
|
+
|
12
|
+
def encode_json(raw)
|
13
|
+
JSON.generate(raw)
|
14
|
+
end
|
15
|
+
|
16
|
+
else
|
17
|
+
require "multi_json"
|
18
|
+
|
19
|
+
def decode_json(encoded)
|
20
|
+
MultiJson.decode(encoded)
|
21
|
+
rescue MultiJson::LoadError
|
22
|
+
raise JWT::DecodeError.new("Invalid segment encoding")
|
23
|
+
end
|
24
|
+
|
25
|
+
def encode_json(raw)
|
26
|
+
MultiJson.encode(raw)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/spec/jwt_spec.rb
CHANGED
@@ -9,24 +9,24 @@ describe JWT do
|
|
9
9
|
secret = "secret"
|
10
10
|
jwt = JWT.encode(@payload, secret)
|
11
11
|
decoded_payload = JWT.decode(jwt, secret)
|
12
|
-
decoded_payload.
|
12
|
+
expect(decoded_payload).to include(@payload)
|
13
13
|
end
|
14
14
|
|
15
15
|
it "encodes and decodes JWTs for RSA signatures" do
|
16
16
|
private_key = OpenSSL::PKey::RSA.generate(512)
|
17
17
|
jwt = JWT.encode(@payload, private_key, "RS256")
|
18
18
|
decoded_payload = JWT.decode(jwt, private_key.public_key)
|
19
|
-
decoded_payload.
|
19
|
+
expect(decoded_payload).to include(@payload)
|
20
20
|
end
|
21
21
|
|
22
22
|
it "encodes and decodes JWTs with custom header fields" do
|
23
23
|
private_key = OpenSSL::PKey::RSA.generate(512)
|
24
24
|
jwt = JWT.encode(@payload, private_key, "RS256", {"kid" => 'default'})
|
25
25
|
decoded_payload = JWT.decode(jwt) do |header|
|
26
|
-
header["kid"].
|
26
|
+
expect(header["kid"]).to eq('default')
|
27
27
|
private_key.public_key
|
28
28
|
end
|
29
|
-
decoded_payload.
|
29
|
+
expect(decoded_payload).to include(@payload)
|
30
30
|
end
|
31
31
|
|
32
32
|
it "decodes valid JWTs" do
|
@@ -34,43 +34,46 @@ describe JWT do
|
|
34
34
|
example_secret = 'secret'
|
35
35
|
example_jwt = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJoZWxsbyI6ICJ3b3JsZCJ9.tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8'
|
36
36
|
decoded_payload = JWT.decode(example_jwt, example_secret)
|
37
|
-
decoded_payload.
|
37
|
+
expect(decoded_payload).to include(example_payload)
|
38
38
|
end
|
39
39
|
|
40
40
|
it "raises exception when the token is invalid" do
|
41
41
|
example_secret = 'secret'
|
42
42
|
# Same as above exmaple with some random bytes replaced
|
43
43
|
example_jwt = 'eyJhbGciOiAiSFMyNTYiLCAidHiMomlwIjogIkJ9.eyJoZWxsbyI6ICJ3b3JsZCJ9.tvagLDLoaiJKxOKqpBXSEGy7SYSifZhjntgm9ctpyj8'
|
44
|
-
|
44
|
+
expect { JWT.decode(example_jwt, example_secret) }.to raise_error(JWT::DecodeError)
|
45
45
|
end
|
46
46
|
|
47
47
|
it "raises exception with wrong hmac key" do
|
48
48
|
right_secret = 'foo'
|
49
49
|
bad_secret = 'bar'
|
50
50
|
jwt_message = JWT.encode(@payload, right_secret, "HS256")
|
51
|
-
|
51
|
+
expect { JWT.decode(jwt_message, bad_secret) }.to raise_error(JWT::DecodeError)
|
52
52
|
end
|
53
53
|
|
54
54
|
it "raises exception with wrong rsa key" do
|
55
55
|
right_private_key = OpenSSL::PKey::RSA.generate(512)
|
56
56
|
bad_private_key = OpenSSL::PKey::RSA.generate(512)
|
57
57
|
jwt = JWT.encode(@payload, right_private_key, "RS256")
|
58
|
-
|
58
|
+
expect { JWT.decode(jwt, bad_private_key.public_key) }.to raise_error(JWT::DecodeError)
|
59
59
|
end
|
60
60
|
|
61
61
|
it "raises exception with invalid signature" do
|
62
|
-
example_payload = {"hello" => "world"}
|
63
62
|
example_secret = 'secret'
|
64
63
|
example_jwt = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJoZWxsbyI6ICJ3b3JsZCJ9.'
|
65
|
-
|
64
|
+
expect { JWT.decode(example_jwt, example_secret) }.to raise_error(JWT::DecodeError)
|
66
65
|
end
|
67
66
|
|
68
67
|
it "raises exception with nonexistent header" do
|
69
|
-
|
68
|
+
expect { JWT.decode("..stuff") }.to raise_error(JWT::DecodeError)
|
70
69
|
end
|
71
70
|
|
72
71
|
it "raises exception with nonexistent payload" do
|
73
|
-
|
72
|
+
expect { JWT.decode("eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9..stuff") }.to raise_error(JWT::DecodeError)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "raises exception with nil jwt" do
|
76
|
+
expect { JWT.decode(nil) }.to raise_error(JWT::DecodeError)
|
74
77
|
end
|
75
78
|
|
76
79
|
it "allows decoding without key" do
|
@@ -78,7 +81,7 @@ describe JWT do
|
|
78
81
|
bad_secret = 'bar'
|
79
82
|
jwt = JWT.encode(@payload, right_secret)
|
80
83
|
decoded_payload = JWT.decode(jwt, bad_secret, false)
|
81
|
-
decoded_payload.
|
84
|
+
expect(decoded_payload).to include(@payload)
|
82
85
|
end
|
83
86
|
|
84
87
|
it "checks the key when verify is truthy" do
|
@@ -86,18 +89,24 @@ describe JWT do
|
|
86
89
|
bad_secret = 'bar'
|
87
90
|
jwt = JWT.encode(@payload, right_secret)
|
88
91
|
verify = "yes" =~ /^y/i
|
89
|
-
|
92
|
+
expect { JWT.decode(jwt, bad_secret, verify) }.to raise_error(JWT::DecodeError)
|
90
93
|
end
|
91
94
|
|
92
95
|
it "raises exception on unsupported crypto algorithm" do
|
93
|
-
|
96
|
+
expect { JWT.encode(@payload, "secret", 'HS1024') }.to raise_error(NotImplementedError)
|
94
97
|
end
|
95
98
|
|
96
99
|
it "encodes and decodes plaintext JWTs" do
|
97
100
|
jwt = JWT.encode(@payload, nil, nil)
|
98
|
-
jwt.split('.').length.
|
101
|
+
expect(jwt.split('.').length).to eq(2)
|
99
102
|
decoded_payload = JWT.decode(jwt, nil, nil)
|
100
|
-
decoded_payload.
|
103
|
+
expect(decoded_payload).to include(@payload)
|
104
|
+
end
|
105
|
+
|
106
|
+
it "requires a signature segment when verify is truthy" do
|
107
|
+
jwt = JWT.encode(@payload, nil, nil)
|
108
|
+
expect(jwt.split('.').length).to eq(2)
|
109
|
+
expect { JWT.decode(jwt, nil, true) }.to raise_error(JWT::DecodeError)
|
101
110
|
end
|
102
111
|
|
103
112
|
it "does not use == to compare digests" do
|
@@ -106,9 +115,9 @@ describe JWT do
|
|
106
115
|
crypto_segment = jwt.split(".").last
|
107
116
|
|
108
117
|
signature = JWT.base64url_decode(crypto_segment)
|
109
|
-
signature.
|
110
|
-
JWT.
|
111
|
-
JWT.
|
118
|
+
expect(signature).not_to receive('==')
|
119
|
+
expect(JWT).to receive(:base64url_decode).with(crypto_segment).once.and_return(signature)
|
120
|
+
expect(JWT).to receive(:base64url_decode).at_least(:once).and_call_original
|
112
121
|
|
113
122
|
JWT.decode(jwt, secret)
|
114
123
|
end
|
@@ -125,14 +134,14 @@ describe JWT do
|
|
125
134
|
end
|
126
135
|
end
|
127
136
|
|
128
|
-
it "retuns
|
129
|
-
|
137
|
+
it "retuns false if the strings are different" do
|
138
|
+
expect(JWT.secure_compare("Foo", "Bar")).to be_false
|
130
139
|
end
|
131
140
|
end
|
132
141
|
|
133
142
|
# no method should leave OpenSSL.errors populated
|
134
143
|
after do
|
135
|
-
OpenSSL.errors.
|
144
|
+
expect(OpenSSL.errors).to be_empty
|
136
145
|
end
|
137
146
|
|
138
147
|
it "raise exception on invalid signature" do
|
@@ -157,13 +166,24 @@ PUBKEY
|
|
157
166
|
'wcy1PxsROY1fmBvXSer0IQesAqOW-rPOCNReSn-eY8d53ph1x2HAF-AzEi3GOl' +
|
158
167
|
'6hFycH8wj7Su6JqqyEbIVLxE7q7DkAZGaMPkxbTHs1EhSd5_oaKQ6O4xO3ZnnT4'
|
159
168
|
)
|
160
|
-
|
169
|
+
expect { JWT.decode(jwt, pubkey, true) }.to raise_error(JWT::DecodeError)
|
161
170
|
end
|
162
171
|
|
163
172
|
describe "urlsafe base64 encoding" do
|
164
173
|
it "replaces + and / with - and _" do
|
165
|
-
Base64.
|
166
|
-
JWT.base64url_encode("foo").
|
174
|
+
allow(Base64).to receive(:encode64) { "string+with/non+url-safe/characters_" }
|
175
|
+
expect(JWT.base64url_encode("foo")).to eq("string-with_non-url-safe_characters_")
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
describe 'decoded_segments' do
|
180
|
+
it "allows access to the decoded header and payload" do
|
181
|
+
secret = "secret"
|
182
|
+
jwt = JWT.encode(@payload, secret)
|
183
|
+
decoded_segments = JWT.decoded_segments(jwt)
|
184
|
+
expect(decoded_segments.size).to eq(4)
|
185
|
+
expect(decoded_segments[0]).to eq({"typ" => "JWT", "alg" => "HS256"})
|
186
|
+
expect(decoded_segments[1]).to eq(@payload)
|
167
187
|
end
|
168
188
|
end
|
169
189
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jwt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,24 +9,8 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-05-
|
12
|
+
date: 2014-05-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
-
- !ruby/object:Gem::Dependency
|
15
|
-
name: multi_json
|
16
|
-
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
|
-
requirements:
|
19
|
-
- - ! '>='
|
20
|
-
- !ruby/object:Gem::Version
|
21
|
-
version: '1.5'
|
22
|
-
type: :runtime
|
23
|
-
prerelease: false
|
24
|
-
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
|
-
requirements:
|
27
|
-
- - ! '>='
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
version: '1.5'
|
30
14
|
- !ruby/object:Gem::Dependency
|
31
15
|
name: echoe
|
32
16
|
requirement: !ruby/object:Gem::Requirement
|
@@ -49,9 +33,11 @@ executables: []
|
|
49
33
|
extensions: []
|
50
34
|
extra_rdoc_files:
|
51
35
|
- lib/jwt.rb
|
36
|
+
- lib/jwt/json.rb
|
52
37
|
files:
|
53
38
|
- Rakefile
|
54
39
|
- lib/jwt.rb
|
40
|
+
- lib/jwt/json.rb
|
55
41
|
- spec/helper.rb
|
56
42
|
- spec/jwt_spec.rb
|
57
43
|
- Manifest
|