sandal 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.coveralls.yml +1 -0
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/.travis.yml +7 -0
- data/.yardopts +6 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +2 -0
- data/LICENSE.md +7 -0
- data/README.md +96 -0
- data/Rakefile +7 -0
- data/lib/sandal.rb +116 -46
- data/lib/sandal/enc.rb +2 -2
- data/lib/sandal/enc/aescbc.rb +2 -2
- data/lib/sandal/enc/aesgcm.rb +3 -3
- data/lib/sandal/sig.rb +2 -2
- data/lib/sandal/sig/es.rb +1 -1
- data/lib/sandal/sig/hs.rb +1 -1
- data/lib/sandal/sig/rs.rb +2 -2
- data/lib/sandal/version.rb +1 -1
- data/sandal.gemspec +32 -0
- data/spec/helper.rb +8 -0
- data/spec/sandal/sig/es_spec.rb +152 -0
- data/spec/sandal/sig/hs_spec.rb +32 -0
- data/spec/sandal/sig/rs_spec.rb +35 -0
- data/spec/sandal/util_spec.rb +33 -0
- data/spec/sandal_spec.rb +160 -0
- metadata +154 -11
data/.coveralls.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
service_name: travis-ci
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (C) 2013 Greg Beech
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
**NOTE: This library is pretty new and still has a lot of things that aren't finished or could be improved. Expect bugs and interface changes. Pull requests or feedback very much appreciated.**
|
2
|
+
|
3
|
+
# Sandal [![Build Status](https://travis-ci.org/gregbeech/sandal.png?branch=master)](https://travis-ci.org/gregbeech/sandal) [![Coverage Status](https://coveralls.io/repos/gregbeech/sandal/badge.png?branch=master)](https://coveralls.io/r/gregbeech/sandal)
|
4
|
+
|
5
|
+
A Ruby library for creating and reading [JSON Web Tokens (JWT) draft-06](http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06), supporting [JSON Web Signatures (JWS) draft-08](http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-08) and [JSON Web Encryption (JWE) draft-08](http://tools.ietf.org/html/draft-ietf-jose-json-web-encryption-08). See the [CHANGELOG](CHANGELOG.md) for version history.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'sandal'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install sandal
|
20
|
+
|
21
|
+
## Signed Tokens
|
22
|
+
|
23
|
+
The signing part of the library is a lot smaller and easier to implement than the encryption part, so I focused on that first. All the JWA signature methods are supported:
|
24
|
+
|
25
|
+
- ES256, ES384, ES512 (note: not supported on jruby)
|
26
|
+
- HS256, HS384, HS512
|
27
|
+
- RS256, RS384, RS512
|
28
|
+
- none
|
29
|
+
|
30
|
+
Signing example:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
require 'openssl'
|
34
|
+
require 'sandal'
|
35
|
+
|
36
|
+
claims = {
|
37
|
+
'iss' => 'example.org',
|
38
|
+
'sub' => 'user@example.org',
|
39
|
+
'exp' => (Time.now + 3600).to_i
|
40
|
+
}
|
41
|
+
key = OpenSSL::PKey::EC.new(File.read('/path/to/private_key.pem'))
|
42
|
+
signer = Sandal::Sig::ES256.new(key)
|
43
|
+
token = Sandal.encode_token(claims, signer, {
|
44
|
+
'kid' => 'my key identifier'
|
45
|
+
})
|
46
|
+
```
|
47
|
+
|
48
|
+
Decoding and validating example:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
require 'openssl'
|
52
|
+
require 'sandal'
|
53
|
+
|
54
|
+
claims = Sandal.decode_token(token) do |header|
|
55
|
+
if header['kid'] == 'my key identifier'
|
56
|
+
key = OpenSSL::PKey::EC.new(File.read('/path/to/public_key.pem'))
|
57
|
+
Sandal::Sig::ES256.new(key)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
Keys for these examples can be generated by executing:
|
63
|
+
|
64
|
+
$ openssl ecparam -out private_key.pem -name prime256v1 -genkey
|
65
|
+
$ openssl ec -out public_key.pem -in private_key.pem -pubout
|
66
|
+
|
67
|
+
## Encrypted Tokens
|
68
|
+
|
69
|
+
This part of the library still needs a lot of work. The current version supports the AES/CBC algorithms and RSA1_5 key protection, but expect a lot of changes here. I'd avoid it for now.
|
70
|
+
|
71
|
+
## Validation Options
|
72
|
+
|
73
|
+
You can change the default validation options, for example if you only want to accept tokens from 'example.org' with a maximum clock skew of one minute:
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
Sandal.default! valid_iss: ['example.org'], max_clock_skew: 60
|
77
|
+
```
|
78
|
+
|
79
|
+
Sometimes while developing it can be useful to turn off some validation options just to get things working (don't do this in production!):
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
Sandal.default! validate_signature: false, validate_exp: false
|
83
|
+
```
|
84
|
+
|
85
|
+
These options can also be configured on a per-token basis by using a second `options` parameter in the block passed to the `decode` method.
|
86
|
+
|
87
|
+
## Contributing
|
88
|
+
|
89
|
+
1. Fork it
|
90
|
+
2. Create your feature branch: `git checkout -b my-new-feature`
|
91
|
+
3. Commit your changes: `git commit -am 'Add some feature'`
|
92
|
+
4. Push to the branch: `git push origin my-new-feature`
|
93
|
+
5. Create a new Pull Request
|
94
|
+
|
95
|
+
|
96
|
+
|
data/Rakefile
ADDED
data/lib/sandal.rb
CHANGED
@@ -11,15 +11,45 @@ require 'sandal/enc'
|
|
11
11
|
# A library for creating and reading JSON Web Tokens (JWT).
|
12
12
|
module Sandal
|
13
13
|
|
14
|
+
# The error that is raised when a token is invalid.
|
15
|
+
class TokenError < StandardError; end
|
16
|
+
|
17
|
+
# The default options for token handling.
|
18
|
+
#
|
19
|
+
# max_clock_skew:: The maximum clock skew, in seconds, when validating times.
|
20
|
+
# valid_iss:: A list of valid token issuers, if issuer validation is required.
|
21
|
+
# valid_aud:: A list of valid audiences, if audience validation is required.
|
22
|
+
# validate_exp:: Whether the expiry date of the token is validated.
|
23
|
+
# validate_nbf:: Whether the not-before date of the token is validated.
|
24
|
+
# validate_integrity:: Whether the integrity value of encrypted (JWE) tokens is validated.
|
25
|
+
# validate_signature:: Whether the signature of signed (JWS) tokens is validated.
|
26
|
+
DEFAULT_OPTIONS = {
|
27
|
+
max_clock_skew: 300,
|
28
|
+
valid_iss: [],
|
29
|
+
valid_aud: [],
|
30
|
+
validate_exp: true,
|
31
|
+
validate_nbf: true,
|
32
|
+
validate_integrity: true,
|
33
|
+
validate_signature: true
|
34
|
+
}
|
35
|
+
|
36
|
+
# Overrides the default options.
|
37
|
+
#
|
38
|
+
# @param defaults [Hash] The options to override (see {DEFAULT_OPTIONS} for details).
|
39
|
+
# @return [Hash] The new default options.
|
40
|
+
def self.default!(defaults)
|
41
|
+
DEFAULT_OPTIONS.merge!(defaults)
|
42
|
+
end
|
43
|
+
|
14
44
|
# Creates a signed JSON Web Token.
|
15
45
|
#
|
16
|
-
# @param payload [String] The payload of the token.
|
46
|
+
# @param payload [String/Hash] The payload of the token. If a Hash then it will be encoded as JSON.
|
17
47
|
# @param signer [Sandal::Sig] The token signer, which may be nil for an unsigned token.
|
18
48
|
# @param header_fields [Hash] Header fields for the token (note: do not include 'alg').
|
19
49
|
# @return [String] A signed JSON Web Token.
|
20
50
|
def self.encode_token(payload, signer, header_fields = nil)
|
21
51
|
if header_fields && header_fields['enc']
|
22
|
-
|
52
|
+
raise ArgumentError, 'The header cannot contain an "enc" parameter.'
|
23
53
|
end
|
24
54
|
signer ||= Sandal::Sig::None.instance
|
25
55
|
|
@@ -27,6 +57,8 @@ module Sandal
|
|
27
57
|
header['alg'] = signer.name if signer.name != Sandal::Sig::None.instance.name
|
28
58
|
header = header_fields.merge(header) if header_fields
|
29
59
|
|
60
|
+
payload = JSON.generate(payload) if payload.kind_of?(Hash)
|
61
|
+
|
30
62
|
encoded_header = Sandal::Util.base64_encode(JSON.generate(header))
|
31
63
|
encoded_payload = Sandal::Util.base64_encode(payload)
|
32
64
|
secured_input = [encoded_header, encoded_payload].join('.')
|
@@ -39,7 +71,7 @@ module Sandal
|
|
39
71
|
# Creates an encrypted JSON Web Token.
|
40
72
|
#
|
41
73
|
# @param payload [String] The payload of the token.
|
42
|
-
# @param
|
74
|
+
# @param encrypter [Sandal::Enc] The token encrypter.
|
43
75
|
# @param header_fields [Hash] Header fields for the token (note: do not include 'alg' or 'enc').
|
44
76
|
# @return [String] An encrypted JSON Web Token.
|
45
77
|
def self.encrypt_token(payload, encrypter, header_fields = nil)
|
@@ -51,30 +83,50 @@ module Sandal
|
|
51
83
|
encrypter.encrypt(header, payload)
|
52
84
|
end
|
53
85
|
|
54
|
-
# Decodes a JSON Web Token
|
86
|
+
# Decodes and validates a JSON Web Token.
|
55
87
|
#
|
56
|
-
#
|
57
|
-
|
88
|
+
# The block is called with the token header as the first parameter, and should return the appropriate
|
89
|
+
# {Sandal::Sig} to verify the signature. It can optionally have a second options parameter which can
|
90
|
+
# be used to override the {DEFAULT_OPTIONS} on a per-token basis.
|
91
|
+
#
|
92
|
+
# @param token [String] The encoded JSON Web Token.
|
93
|
+
# @yieldparam header [Hash] The JWT header values.
|
94
|
+
# @yieldparam options [Hash] (Optional) A hash that can be used to override the default options.
|
95
|
+
# @yieldreturn [Sandal::Sig] The signature verifier.
|
96
|
+
# @return [Hash/String] The payload of the token as a Hash if it was JSON, otherwise as a String.
|
97
|
+
# @raise [ArgumentError] The token parameter is nil or empty, or the block has the wrong arity.
|
98
|
+
# @raise [TokenError] The token format is invalid, or validation of the token failed.
|
99
|
+
def self.decode_token(token, &block)
|
100
|
+
raise ArgumentError, 'A token is required.' unless token && token.length > 0
|
58
101
|
parts = token.split('.')
|
59
|
-
|
102
|
+
raise TokenError, 'Invalid token format.' unless [2, 3].include?(parts.length)
|
60
103
|
begin
|
61
104
|
header = JSON.parse(Sandal::Util.base64_decode(parts[0]))
|
62
105
|
payload = Sandal::Util.base64_decode(parts[1])
|
63
106
|
signature = if parts.length > 2 then Sandal::Util.base64_decode(parts[2]) else '' end
|
64
107
|
rescue
|
65
|
-
|
108
|
+
raise TokenError, 'Invalid token encoding.'
|
66
109
|
end
|
67
110
|
|
68
|
-
|
69
|
-
if
|
70
|
-
|
71
|
-
|
72
|
-
|
111
|
+
options = DEFAULT_OPTIONS.clone
|
112
|
+
if block
|
113
|
+
case block.arity
|
114
|
+
when 1 then verifier = block.call(header)
|
115
|
+
when 2 then verifier = block.call(header, options)
|
116
|
+
else raise ArgumentError, 'Incorrect number of block parameters.'
|
117
|
+
end
|
118
|
+
end
|
119
|
+
verifier ||= Sandal::Sig::None.instance
|
120
|
+
|
121
|
+
if options[:validate_signature]
|
73
122
|
secured_input = parts.take(2).join('.')
|
74
|
-
|
123
|
+
raise TokenError, 'Invalid signature.' unless verifier.verify(signature, secured_input)
|
75
124
|
end
|
76
125
|
|
77
|
-
payload
|
126
|
+
claims = JSON.parse(payload) rescue nil
|
127
|
+
validate_claims(claims, options) if claims
|
128
|
+
|
129
|
+
claims || payload
|
78
130
|
end
|
79
131
|
|
80
132
|
# Decrypts an encrypted JSON Web Token.
|
@@ -82,7 +134,7 @@ module Sandal
|
|
82
134
|
# **NOTE: This method is likely to change, to allow more validation options**
|
83
135
|
def self.decrypt_token(encrypted_token, &enc_finder)
|
84
136
|
parts = encrypted_token.split('.')
|
85
|
-
|
137
|
+
raise ArgumentError, 'Invalid token format.' unless parts.length == 5
|
86
138
|
begin
|
87
139
|
header = JSON.parse(Sandal::Util.base64_decode(parts[0]))
|
88
140
|
encrypted_key = Sandal::Util.base64_decode(parts[1])
|
@@ -90,46 +142,64 @@ module Sandal
|
|
90
142
|
ciphertext = Sandal::Util.base64_decode(parts[3])
|
91
143
|
integrity_value = Sandal::Util.base64_decode(parts[4])
|
92
144
|
rescue
|
93
|
-
|
145
|
+
raise ArgumentError, 'Invalid token encoding.'
|
94
146
|
end
|
95
147
|
|
96
148
|
enc = enc_finder.call(header)
|
97
|
-
|
149
|
+
raise TokenError, 'No decryptor was found.' unless enc
|
98
150
|
enc.decrypt(encrypted_key, iv, ciphertext, parts.take(4).join('.'), integrity_value)
|
99
151
|
end
|
100
152
|
|
101
|
-
|
102
|
-
|
103
|
-
if __FILE__ == $0
|
104
|
-
|
105
|
-
# create payload
|
106
|
-
issued_at = Time.now
|
107
|
-
claims = JSON.generate({
|
108
|
-
iss: 'example.org',
|
109
|
-
aud: 'example.com',
|
110
|
-
sub: 'user@example.org',
|
111
|
-
iat: issued_at.to_i,
|
112
|
-
exp: (issued_at + 3600).to_i
|
113
|
-
})
|
153
|
+
private
|
114
154
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
puts jws_token
|
155
|
+
# Validates token claims according to the options
|
156
|
+
def self.validate_claims(claims, options)
|
157
|
+
validate_expires(claims, options)
|
158
|
+
validate_not_before(claims, options)
|
159
|
+
validate_issuer(claims, options)
|
160
|
+
validate_audience(claims, options)
|
161
|
+
end
|
123
162
|
|
124
|
-
|
125
|
-
|
126
|
-
|
163
|
+
# Validates the 'exp' claim.
|
164
|
+
def self.validate_expires(claims, options)
|
165
|
+
if options[:validate_exp] && claims['exp']
|
166
|
+
begin
|
167
|
+
exp = Time.at(claims['exp'])
|
168
|
+
rescue
|
169
|
+
raise TokenError, 'The "exp" claim is invalid.'
|
170
|
+
end
|
171
|
+
raise TokenError, 'The token has expired.' unless exp > (Time.now - options[:max_clock_skew])
|
172
|
+
end
|
173
|
+
end
|
127
174
|
|
128
|
-
|
175
|
+
# Validates the 'nbf' claim
|
176
|
+
def self.validate_not_before(claims, options)
|
177
|
+
if options[:validate_nbf] && claims['nbf']
|
178
|
+
begin
|
179
|
+
nbf = Time.at(claims['nbf'])
|
180
|
+
rescue
|
181
|
+
raise TokenError, 'The "nbf" claim is invalid.'
|
182
|
+
end
|
183
|
+
raise TokenError, 'The token is not valid yet.' unless nbf < (Time.now + options[:max_clock_skew])
|
184
|
+
end
|
185
|
+
end
|
129
186
|
|
130
|
-
|
131
|
-
|
187
|
+
# Validates the 'iss' claim.
|
188
|
+
def self.validate_issuer(claims, options)
|
189
|
+
valid_iss = options[:valid_iss]
|
190
|
+
if valid_iss && valid_iss.length > 0
|
191
|
+
raise TokenError, 'The issuer is invalid.' unless valid_iss.include?(claims['iss'])
|
192
|
+
end
|
193
|
+
end
|
132
194
|
|
133
|
-
|
195
|
+
# Validates the 'aud' claim.
|
196
|
+
def self.validate_audience(claims, options)
|
197
|
+
valid_aud = options[:valid_aud]
|
198
|
+
if valid_aud && valid_aud.length > 0
|
199
|
+
aud = claims['aud']
|
200
|
+
aud = [aud] unless aud.kind_of?(Array)
|
201
|
+
raise TokenError, 'The audence is invalid.' unless (aud & valid_aud).length > 0
|
202
|
+
end
|
203
|
+
end
|
134
204
|
|
135
205
|
end
|
data/lib/sandal/enc.rb
CHANGED
@@ -10,12 +10,12 @@ module Sandal
|
|
10
10
|
|
11
11
|
# Encrypts a header and payload, and returns an encrypted token.
|
12
12
|
def encrypt(header, payload)
|
13
|
-
|
13
|
+
raise NotImplementedError, "#{@name}.encrypt is not implemented."
|
14
14
|
end
|
15
15
|
|
16
16
|
# Decrypts a token.
|
17
17
|
def decrypt(encrypted_key, iv, ciphertext, secured_input, integrity_value)
|
18
|
-
|
18
|
+
raise NotImplementedError, "#{@name}.decrypt is not implemented."
|
19
19
|
end
|
20
20
|
|
21
21
|
end
|
data/lib/sandal/enc/aescbc.rb
CHANGED
@@ -9,7 +9,7 @@ module Sandal
|
|
9
9
|
include Sandal::Enc
|
10
10
|
|
11
11
|
def initialize(aes_size, key)
|
12
|
-
|
12
|
+
raise ArgumentError, 'A key is required.' unless key
|
13
13
|
@aes_size = aes_size
|
14
14
|
@sha_size = aes_size * 2 # TODO: Any smarter way to do this?
|
15
15
|
@name = "A#{aes_size}CBC+HS#{@sha_size}"
|
@@ -49,7 +49,7 @@ module Sandal
|
|
49
49
|
|
50
50
|
content_integrity_key = derive_content_key('Integrity', content_master_key, @sha_size)
|
51
51
|
computed_integrity_value = OpenSSL::HMAC.digest(@digest, content_integrity_key, secured_input)
|
52
|
-
|
52
|
+
raise ArgumentError, 'Invalid signature.' unless integrity_value == computed_integrity_value
|
53
53
|
|
54
54
|
cipher = OpenSSL::Cipher.new(@cipher_name).decrypt
|
55
55
|
cipher.key = derive_content_key('Encryption', content_master_key, @aes_size)
|
data/lib/sandal/enc/aesgcm.rb
CHANGED
@@ -9,15 +9,15 @@ module Sandal
|
|
9
9
|
include Sandal::Enc
|
10
10
|
|
11
11
|
def initialize(aes_size, key)
|
12
|
-
|
12
|
+
raise NotImplementedException, 'AES-CGM is not yet implemented.'
|
13
13
|
end
|
14
14
|
|
15
15
|
def encrypt(header, payload)
|
16
|
-
|
16
|
+
raise NotImplementedException, 'AES-CGM is not yet implemented.'
|
17
17
|
end
|
18
18
|
|
19
19
|
def decrypt(encrypted_key, iv, ciphertext, secured_input, integrity_value)
|
20
|
-
|
20
|
+
raise NotImplementedException, 'AES-CGM is not yet implemented.'
|
21
21
|
end
|
22
22
|
|
23
23
|
end
|
data/lib/sandal/sig.rb
CHANGED
@@ -12,7 +12,7 @@ module Sandal
|
|
12
12
|
# @param payload [String] The payload of the token to sign.
|
13
13
|
# @return [String] The signature.
|
14
14
|
def sign(payload)
|
15
|
-
|
15
|
+
raise NotImplementedError, "#{@name}.sign is not implemented."
|
16
16
|
end
|
17
17
|
|
18
18
|
# Verifies a payload signature and returns whether the signature matches.
|
@@ -21,7 +21,7 @@ module Sandal
|
|
21
21
|
# @param payload [String] The payload of the token.
|
22
22
|
# @return [Boolean] true if the signature is correct; otherwise false.
|
23
23
|
def verify(signature, payload)
|
24
|
-
|
24
|
+
raise NotImplementedError, "#{@name}.verify is not implemented."
|
25
25
|
end
|
26
26
|
|
27
27
|
# The 'none' JWA signature method.
|
data/lib/sandal/sig/es.rb
CHANGED
@@ -9,7 +9,7 @@ module Sandal
|
|
9
9
|
|
10
10
|
# Creates a new instance with the size of the SHA algorithm and an OpenSSL ES PKey.
|
11
11
|
def initialize(sha_size, prime_size, key)
|
12
|
-
|
12
|
+
raise ArgumentError, 'A key is required.' unless key
|
13
13
|
@name = "ES#{sha_size}"
|
14
14
|
@digest = OpenSSL::Digest.new("sha#{sha_size}")
|
15
15
|
@prime_size = prime_size
|
data/lib/sandal/sig/hs.rb
CHANGED
@@ -9,7 +9,7 @@ module Sandal
|
|
9
9
|
|
10
10
|
# Creates a new instance with the size of the SHA algorithm and a string key.
|
11
11
|
def initialize(sha_size, key)
|
12
|
-
|
12
|
+
raise ArgumentError, 'A key is required.' unless key
|
13
13
|
@name = "HS#{sha_size}"
|
14
14
|
@digest = OpenSSL::Digest.new("sha#{sha_size}")
|
15
15
|
@key = key
|
data/lib/sandal/sig/rs.rb
CHANGED
@@ -12,7 +12,7 @@ module Sandal
|
|
12
12
|
# Note that the size of the RSA key must be at least 2048 bits to be compliant with the
|
13
13
|
# JWA specification.
|
14
14
|
def initialize(sha_size, key)
|
15
|
-
|
15
|
+
raise ArgumentError, 'A key is required.' unless key
|
16
16
|
@name = "RS#{sha_size}"
|
17
17
|
@digest = OpenSSL::Digest.new("sha#{sha_size}")
|
18
18
|
@key = key
|
@@ -20,7 +20,7 @@ module Sandal
|
|
20
20
|
|
21
21
|
# Signs a payload and returns the signature.
|
22
22
|
def sign(payload)
|
23
|
-
|
23
|
+
raise ArgumentError, 'A private key is required to sign the payload.' unless @key.private?
|
24
24
|
@key.sign(@digest, payload)
|
25
25
|
end
|
26
26
|
|
data/lib/sandal/version.rb
CHANGED
data/sandal.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
($LOAD_PATH << File.expand_path("../lib", __FILE__)).uniq!
|
2
|
+
require 'sandal/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'sandal'
|
6
|
+
s.version = Sandal::VERSION
|
7
|
+
s.summary = 'A JSON Web Token (JWT) library.'
|
8
|
+
s.description = 'A ruby library for creating and reading JSON Web Tokens (JWT), supporting JSON Web Signatures (JWS) and JSON Web Encryption (JWE).'
|
9
|
+
s.author = 'Greg Beech'
|
10
|
+
s.email = 'greg@gregbeech.com'
|
11
|
+
s.homepage = 'http://rubygems.org/gems/sandal'
|
12
|
+
s.license = 'MIT'
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split($/)
|
15
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
16
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
17
|
+
s.require_paths = ['lib']
|
18
|
+
s.extra_rdoc_files = ['README.md', 'LICENSE.md']
|
19
|
+
|
20
|
+
s.add_runtime_dependency 'json', '~> 1.7'
|
21
|
+
s.add_runtime_dependency 'jruby-openssl', '~> 0.7', '>= 0.7.3' if RUBY_PLATFORM == 'java'
|
22
|
+
|
23
|
+
s.add_development_dependency 'bundler', '~> 1.3'
|
24
|
+
s.add_development_dependency 'rake', '~> 10.0'
|
25
|
+
s.add_development_dependency 'rspec', '~> 2.13'
|
26
|
+
s.add_development_dependency 'coveralls', '~> 0.6'
|
27
|
+
s.add_development_dependency 'yard', '~> 0.8'
|
28
|
+
s.add_development_dependency 'redcarpet', '~> 2.2' unless RUBY_PLATFORM == 'java' # for yard
|
29
|
+
s.add_development_dependency 'kramdown', '~> 1.0' if RUBY_PLATFORM == 'java' # for yard
|
30
|
+
|
31
|
+
s.requirements << 'openssl 1.0.1c for EC signature methods'
|
32
|
+
end
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
# EC isn't implemented in jruby-openssl at the moment
|
5
|
+
if defined? OpenSSL::PKey::EC
|
6
|
+
|
7
|
+
def make_bn(arr)
|
8
|
+
hex_str = arr.pack('C*').unpack('H*')[0]
|
9
|
+
OpenSSL::BN.new(hex_str, 16)
|
10
|
+
end
|
11
|
+
|
12
|
+
def make_point(group, x, y)
|
13
|
+
def pad(c)
|
14
|
+
if c.length <= 64
|
15
|
+
padding_length = 64 - c.length
|
16
|
+
elsif c.length <= 96
|
17
|
+
padding_length = 96 - c.length
|
18
|
+
elsif c.length <= 132
|
19
|
+
padding_length = 132 - c.length
|
20
|
+
end
|
21
|
+
('0' * padding_length) + c
|
22
|
+
end
|
23
|
+
str = '04' + pad(x.to_s(16)) + pad(y.to_s(16))
|
24
|
+
bn = OpenSSL::BN.new(str, 16)
|
25
|
+
OpenSSL::PKey::EC::Point.new(group, bn)
|
26
|
+
end
|
27
|
+
|
28
|
+
describe Sandal::Sig::ES do
|
29
|
+
|
30
|
+
it 'can encode the signature in JWS section A3.1' do
|
31
|
+
r = make_bn([14, 209, 33, 83, 121, 99, 108, 72, 60, 47, 127, 21, 88, 7, 212, 2, 163, 178, 40, 3, 58, 249, 124, 126, 23, 129, 154, 195, 22, 158, 166, 101] )
|
32
|
+
s = make_bn([197, 10, 7, 211, 140, 60, 112, 229, 216, 241, 45, 175, 8, 74, 84, 128, 166, 101, 144, 197, 242, 147, 80, 154, 143, 63, 127, 138, 131, 163, 84, 213])
|
33
|
+
signature = Sandal::Sig::ES.encode_jws_signature(r, s, 256)
|
34
|
+
base64_signature = Sandal::Util.base64_encode(signature)
|
35
|
+
base64_signature.should == 'DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q'
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'can encode the signature in JWS section A4.1' do
|
39
|
+
r = make_bn([1, 220, 12, 129, 231, 171, 194, 209, 232, 135, 233, 117, 247, 105, 122, 210, 26, 125, 192, 1, 217, 21, 82, 91, 45, 240, 255, 83, 19, 34, 239, 71, 48, 157, 147, 152, 105, 18, 53, 108, 163, 214, 68, 231, 62, 153, 150, 106, 194, 164, 246, 72, 143, 138, 24, 50, 129, 223, 133, 206, 209, 172, 63, 237, 119, 109] )
|
40
|
+
s = make_bn([0, 111, 6, 105, 44, 5, 41, 208, 128, 61, 152, 40, 92, 61, 152, 4, 150, 66, 60, 69, 247, 196, 170, 81, 193, 199, 78, 59, 194, 169, 16, 124, 9, 143, 42, 142, 131, 48, 206, 238, 34, 175, 83, 203, 220, 159, 3, 107, 155, 22, 27, 73, 111, 68, 68, 21, 238, 144, 229, 232, 148, 188, 222, 59, 242, 103] )
|
41
|
+
signature = Sandal::Sig::ES.encode_jws_signature(r, s, 521)
|
42
|
+
base64_signature = Sandal::Util.base64_encode(signature)
|
43
|
+
base64_signature.should == 'AdwMgeerwtHoh-l192l60hp9wAHZFVJbLfD_UxMi70cwnZOYaRI1bKPWROc-mZZqwqT2SI-KGDKB34XO0aw_7XdtAG8GaSwFKdCAPZgoXD2YBJZCPEX3xKpRwcdOO8KpEHwJjyqOgzDO7iKvU8vcnwNrmxYbSW9ERBXukOXolLzeO_Jn'
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
describe Sandal::Sig::ES256 do
|
49
|
+
|
50
|
+
it 'can sign data and verify signatures' do
|
51
|
+
group = OpenSSL::PKey::EC::Group.new('prime256v1')
|
52
|
+
private_key = OpenSSL::PKey::EC.new(group).generate_key
|
53
|
+
data = 'Hello ES256'
|
54
|
+
signer = Sandal::Sig::ES256.new(private_key)
|
55
|
+
signature = signer.sign(data)
|
56
|
+
public_key = OpenSSL::PKey::EC.new(group)
|
57
|
+
public_key.public_key = private_key.public_key
|
58
|
+
verifier = Sandal::Sig::ES256.new(public_key)
|
59
|
+
verifier.verify(signature, data).should == true
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'can verify the signature in JWS section A3.1' do
|
63
|
+
x = make_bn([127, 205, 206, 39, 112, 246, 196, 93, 65, 131, 203, 238, 111, 219, 75, 123, 88, 7, 51, 53, 123, 233, 239, 19, 186, 207, 110, 60, 123, 209, 84, 69])
|
64
|
+
y = make_bn([199, 241, 68, 205, 27, 189, 155, 126, 135, 44, 223, 237, 185, 238, 185, 244, 179, 105, 93, 110, 169, 11, 36, 173, 138, 70, 35, 40, 133, 136, 229, 173])
|
65
|
+
d = make_bn([142, 155, 16, 158, 113, 144, 152, 191, 152, 4, 135, 223, 31, 93, 119, 233, 203, 41, 96, 110, 190, 210, 38, 59, 95, 87, 194, 19, 223, 132, 244, 178])
|
66
|
+
data = 'eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ'
|
67
|
+
signature = Sandal::Util.base64_decode('DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q')
|
68
|
+
|
69
|
+
group = OpenSSL::PKey::EC::Group.new('prime256v1')
|
70
|
+
public_key = OpenSSL::PKey::EC.new(group)
|
71
|
+
public_key.public_key = make_point(group, x, y)
|
72
|
+
verifier = Sandal::Sig::ES256.new(public_key)
|
73
|
+
verifier.verify(signature, data).should == true
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'fails to verify the signature in JWS section A3.1 when the data is changed' do
|
77
|
+
x = make_bn([127, 205, 206, 39, 112, 246, 196, 93, 65, 131, 203, 238, 111, 219, 75, 123, 88, 7, 51, 53, 123, 233, 239, 19, 186, 207, 110, 60, 123, 209, 84, 69])
|
78
|
+
y = make_bn([199, 241, 68, 205, 27, 189, 155, 126, 135, 44, 223, 237, 185, 238, 185, 244, 179, 105, 93, 110, 169, 11, 36, 173, 138, 70, 35, 40, 133, 136, 229, 173])
|
79
|
+
d = make_bn([142, 155, 16, 158, 113, 144, 152, 191, 152, 4, 135, 223, 31, 93, 119, 233, 203, 41, 96, 110, 190, 210, 38, 59, 95, 87, 194, 19, 223, 132, 244, 178])
|
80
|
+
data = 'not the data that was signed'
|
81
|
+
signature = Sandal::Util.base64_decode('DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q')
|
82
|
+
|
83
|
+
group = OpenSSL::PKey::EC::Group.new('prime256v1')
|
84
|
+
public_key = OpenSSL::PKey::EC.new(group)
|
85
|
+
public_key.public_key = make_point(group, x, y)
|
86
|
+
verifier = Sandal::Sig::ES256.new(public_key)
|
87
|
+
verifier.verify(signature, data).should == false
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
describe Sandal::Sig::ES384 do
|
93
|
+
|
94
|
+
it 'can sign data and verify signatures' do
|
95
|
+
group = OpenSSL::PKey::EC::Group.new('secp384r1')
|
96
|
+
private_key = OpenSSL::PKey::EC.new(group).generate_key
|
97
|
+
data = 'Hello ES384'
|
98
|
+
signer = Sandal::Sig::ES384.new(private_key)
|
99
|
+
signature = signer.sign(data)
|
100
|
+
public_key = OpenSSL::PKey::EC.new(group)
|
101
|
+
public_key.public_key = private_key.public_key
|
102
|
+
verifier = Sandal::Sig::ES384.new(public_key)
|
103
|
+
verifier.verify(signature, data).should == true
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
describe Sandal::Sig::ES512 do
|
109
|
+
|
110
|
+
it 'can sign data and verify signatures' do
|
111
|
+
group = OpenSSL::PKey::EC::Group.new('secp521r1')
|
112
|
+
private_key = OpenSSL::PKey::EC.new(group).generate_key
|
113
|
+
data = 'Hello ES512'
|
114
|
+
signer = Sandal::Sig::ES512.new(private_key)
|
115
|
+
signature = signer.sign(data)
|
116
|
+
public_key = OpenSSL::PKey::EC.new(group)
|
117
|
+
public_key.public_key = private_key.public_key
|
118
|
+
verifier = Sandal::Sig::ES512.new(public_key)
|
119
|
+
verifier.verify(signature, data).should == true
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'can verify the signature in JWS section A4.1' do
|
123
|
+
x = make_bn([1, 233, 41, 5, 15, 18, 79, 198, 188, 85, 199, 213, 57, 51, 101, 223, 157, 239, 74, 176, 194, 44, 178, 87, 152, 249, 52, 235, 4, 227, 198, 186, 227, 112, 26, 87, 167, 145, 14, 157, 129, 191, 54, 49, 89, 232, 235, 203, 21, 93, 99, 73, 244, 189, 182, 204, 248, 169, 76, 92, 89, 199, 170, 193, 1, 164])
|
124
|
+
y = make_bn([0, 52, 166, 68, 14, 55, 103, 80, 210, 55, 31, 209, 189, 194, 200, 243, 183, 29, 47, 78, 229, 234, 52, 50, 200, 21, 204, 163, 21, 96, 254, 93, 147, 135, 236, 119, 75, 85, 131, 134, 48, 229, 203, 191, 90, 140, 190, 10, 145, 221, 0, 100, 198, 153, 154, 31, 110, 110, 103, 250, 221, 237, 228, 200, 200, 246])
|
125
|
+
d = make_bn([1, 142, 105, 111, 176, 52, 80, 88, 129, 221, 17, 11, 72, 62, 184, 125, 50, 206, 73, 95, 227, 107, 55, 69, 237, 242, 216, 202, 228, 240, 242, 83, 159, 70, 21, 160, 233, 142, 171, 82, 179, 192, 197, 234, 196, 206, 7, 81, 133, 168, 231, 187, 71, 222, 172, 29, 29, 231, 123, 204, 246, 97, 53, 230, 61, 130] )
|
126
|
+
data = 'eyJhbGciOiJFUzUxMiJ9.UGF5bG9hZA'
|
127
|
+
signature = Sandal::Util.base64_decode('AdwMgeerwtHoh-l192l60hp9wAHZFVJbLfD_UxMi70cwnZOYaRI1bKPWROc-mZZqwqT2SI-KGDKB34XO0aw_7XdtAG8GaSwFKdCAPZgoXD2YBJZCPEX3xKpRwcdOO8KpEHwJjyqOgzDO7iKvU8vcnwNrmxYbSW9ERBXukOXolLzeO_Jn')
|
128
|
+
|
129
|
+
group = OpenSSL::PKey::EC::Group.new('secp521r1')
|
130
|
+
public_key = OpenSSL::PKey::EC.new(group)
|
131
|
+
public_key.public_key = make_point(group, x, y)
|
132
|
+
verifier = Sandal::Sig::ES512.new(public_key)
|
133
|
+
verifier.verify(signature, data).should == true
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'fails to verify the signature in JWS section A4.1 when the data is changed' do
|
137
|
+
x = make_bn([1, 233, 41, 5, 15, 18, 79, 198, 188, 85, 199, 213, 57, 51, 101, 223, 157, 239, 74, 176, 194, 44, 178, 87, 152, 249, 52, 235, 4, 227, 198, 186, 227, 112, 26, 87, 167, 145, 14, 157, 129, 191, 54, 49, 89, 232, 235, 203, 21, 93, 99, 73, 244, 189, 182, 204, 248, 169, 76, 92, 89, 199, 170, 193, 1, 164])
|
138
|
+
y = make_bn([0, 52, 166, 68, 14, 55, 103, 80, 210, 55, 31, 209, 189, 194, 200, 243, 183, 29, 47, 78, 229, 234, 52, 50, 200, 21, 204, 163, 21, 96, 254, 93, 147, 135, 236, 119, 75, 85, 131, 134, 48, 229, 203, 191, 90, 140, 190, 10, 145, 221, 0, 100, 198, 153, 154, 31, 110, 110, 103, 250, 221, 237, 228, 200, 200, 246])
|
139
|
+
d = make_bn([1, 142, 105, 111, 176, 52, 80, 88, 129, 221, 17, 11, 72, 62, 184, 125, 50, 206, 73, 95, 227, 107, 55, 69, 237, 242, 216, 202, 228, 240, 242, 83, 159, 70, 21, 160, 233, 142, 171, 82, 179, 192, 197, 234, 196, 206, 7, 81, 133, 168, 231, 187, 71, 222, 172, 29, 29, 231, 123, 204, 246, 97, 53, 230, 61, 130] )
|
140
|
+
data = 'not the data that was signed'
|
141
|
+
signature = Sandal::Util.base64_decode('AdwMgeerwtHoh-l192l60hp9wAHZFVJbLfD_UxMi70cwnZOYaRI1bKPWROc-mZZqwqT2SI-KGDKB34XO0aw_7XdtAG8GaSwFKdCAPZgoXD2YBJZCPEX3xKpRwcdOO8KpEHwJjyqOgzDO7iKvU8vcnwNrmxYbSW9ERBXukOXolLzeO_Jn')
|
142
|
+
|
143
|
+
group = OpenSSL::PKey::EC::Group.new('secp521r1')
|
144
|
+
public_key = OpenSSL::PKey::EC.new(group)
|
145
|
+
public_key.public_key = make_point(group, x, y)
|
146
|
+
verifier = Sandal::Sig::ES512.new(public_key)
|
147
|
+
verifier.verify(signature, data).should == false
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
describe Sandal::Sig::HS256 do
|
5
|
+
it 'can sign data and verify signatures' do
|
6
|
+
data = 'Hello HS256'
|
7
|
+
key = 'A secret key'
|
8
|
+
signer = Sandal::Sig::HS256.new(key)
|
9
|
+
signature = signer.sign(data)
|
10
|
+
signer.verify(signature, data).should == true
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe Sandal::Sig::HS384 do
|
15
|
+
it 'can sign data and verify signatures' do
|
16
|
+
data = 'Hello HS384'
|
17
|
+
key = 'Another secret key'
|
18
|
+
signer = Sandal::Sig::HS384.new(key)
|
19
|
+
signature = signer.sign(data)
|
20
|
+
signer.verify(signature, data).should == true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe Sandal::Sig::HS512 do
|
25
|
+
it 'can sign data and verify signatures' do
|
26
|
+
data = 'Hello HS512'
|
27
|
+
key = 'Yet another secret key'
|
28
|
+
signer = Sandal::Sig::HS512.new(key)
|
29
|
+
signature = signer.sign(data)
|
30
|
+
signer.verify(signature, data).should == true
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
describe Sandal::Sig::RS256 do
|
5
|
+
it 'can sign data and verify signatures' do
|
6
|
+
data = 'Hello RS256'
|
7
|
+
private_key = OpenSSL::PKey::RSA.generate(2048)
|
8
|
+
signer = Sandal::Sig::RS256.new(private_key)
|
9
|
+
signature = signer.sign(data)
|
10
|
+
verifier = Sandal::Sig::RS256.new(private_key.public_key)
|
11
|
+
verifier.verify(signature, data).should == true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe Sandal::Sig::RS384 do
|
16
|
+
it 'can sign data and verify signatures' do
|
17
|
+
data = 'Hello RS384'
|
18
|
+
private_key = OpenSSL::PKey::RSA.generate(2048)
|
19
|
+
signer = Sandal::Sig::RS384.new(private_key)
|
20
|
+
signature = signer.sign(data)
|
21
|
+
verifier = Sandal::Sig::RS384.new(private_key.public_key)
|
22
|
+
verifier.verify(signature, data).should == true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe Sandal::Sig::RS512 do
|
27
|
+
it 'can sign data and verify signatures' do
|
28
|
+
data = 'Hello RS512'
|
29
|
+
private_key = OpenSSL::PKey::RSA.generate(2048)
|
30
|
+
signer = Sandal::Sig::RS512.new(private_key)
|
31
|
+
signature = signer.sign(data)
|
32
|
+
verifier = Sandal::Sig::RS512.new(private_key.public_key)
|
33
|
+
verifier.verify(signature, data).should == true
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
describe Sandal::Util do
|
5
|
+
|
6
|
+
it 'encodes and decodes base64 as per JWT example 6.1' do
|
7
|
+
src = "{\"iss\":\"joe\",\r\n \"exp\":1300819380,\r\n \"http://example.com/is_root\":true}"
|
8
|
+
encoded = Sandal::Util.base64_encode(src)
|
9
|
+
encoded.should == 'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ'
|
10
|
+
val = Sandal::Util.base64_decode(encoded)
|
11
|
+
val.should == src
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'compares nil strings as equal' do
|
15
|
+
Sandal::Util.secure_equals(nil, nil).should == true
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'compares nil strings as unequal to empty strings' do
|
19
|
+
Sandal::Util.secure_equals(nil, '').should == false
|
20
|
+
Sandal::Util.secure_equals('', nil).should == false
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'compares equal strings as equal' do
|
24
|
+
Sandal::Util.secure_equals('hello', 'hello').should == true
|
25
|
+
Sandal::Util.secure_equals('a longer string', 'a longer string').should == true
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'compares unequal strings as unequal' do
|
29
|
+
Sandal::Util.secure_equals('hello', 'world').should == false
|
30
|
+
Sandal::Util.secure_equals('a longer string', 'a different longer string').should == false
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
data/spec/sandal_spec.rb
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
describe Sandal do
|
5
|
+
|
6
|
+
it 'raises a token error when the token format is invalid' do
|
7
|
+
expect { Sandal.decode_token('not a valid token') }.to raise_error Sandal::TokenError
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'raises a token error when the token encoding is invalid' do
|
11
|
+
expect { Sandal.decode_token('an.invalid.token') }.to raise_error Sandal::TokenError
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'encodes and decodes tokens with no signature' do
|
15
|
+
payload = 'Hello, World'
|
16
|
+
token = Sandal.encode_token(payload, nil)
|
17
|
+
decoded_payload = Sandal.decode_token(token)
|
18
|
+
decoded_payload.should == payload
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'encodes and decodes tokens with "none" signature' do
|
22
|
+
payload = 'Hello, World'
|
23
|
+
token = Sandal.encode_token(payload, Sandal::Sig::None.instance)
|
24
|
+
decoded_payload = Sandal.decode_token(token)
|
25
|
+
decoded_payload.should == payload
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'decodes non-JSON payloads to a String' do
|
29
|
+
token = Sandal.encode_token('not valid json', nil)
|
30
|
+
Sandal.decode_token(token).class.should == String
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'decodes JSON payloads to a Hash' do
|
34
|
+
token = Sandal.encode_token({ 'valid' => 'json' }, nil)
|
35
|
+
Sandal.decode_token(token).class.should == Hash
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'raises a token error when the expiry date is far in the past' do
|
39
|
+
token = Sandal.encode_token({ 'exp' => (Time.now - 600).to_i }, nil)
|
40
|
+
expect { Sandal.decode_token(token) }.to raise_error Sandal::TokenError
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'raises a token error when the expiry date is invalid' do
|
44
|
+
token = Sandal.encode_token({ 'exp' => 'invalid value' }, nil)
|
45
|
+
expect { Sandal.decode_token(token) }.to raise_error Sandal::TokenError
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'does not raise an error when the expiry date is far in the past but validation is disabled' do
|
49
|
+
token = Sandal.encode_token({ 'exp' => (Time.now - 600).to_i }, nil)
|
50
|
+
Sandal.decode_token(token) { |header, options| options[:validate_exp] = false }
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'does not raise an error when the expiry date is in the past but within the clock skew' do
|
54
|
+
token = Sandal.encode_token({ 'exp' => (Time.now - 60).to_i }, nil)
|
55
|
+
Sandal.decode_token(token)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'does not raise an error when the expiry date is valid' do
|
59
|
+
token = Sandal.encode_token({ 'exp' => (Time.now + 60).to_i }, nil)
|
60
|
+
Sandal.decode_token(token)
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'raises a token error when the not-before date is far in the future' do
|
64
|
+
token = Sandal.encode_token({ 'nbf' => (Time.now + 600).to_i }, nil)
|
65
|
+
expect { Sandal.decode_token(token) }.to raise_error Sandal::TokenError
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'raises a token error when the not-before date is invalid' do
|
69
|
+
token = Sandal.encode_token({ 'nbf' => 'invalid value' }, nil)
|
70
|
+
expect { Sandal.decode_token(token) }.to raise_error Sandal::TokenError
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'does not raise an error when the not-before date is far in the future but validation is disabled' do
|
74
|
+
token = Sandal.encode_token({ 'nbf' => (Time.now + 600).to_i }, nil)
|
75
|
+
Sandal.decode_token(token) { |header, options| options[:validate_nbf] = false }
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'does not raise an error when the not-before date is in the future but within the clock skew' do
|
79
|
+
token = Sandal.encode_token({ 'nbf' => (Time.now + 60).to_i }, nil)
|
80
|
+
Sandal.decode_token(token)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'does not raise an error when the not-before is valid' do
|
84
|
+
token = Sandal.encode_token({ 'nbf' => (Time.now - 60).to_i }, nil)
|
85
|
+
Sandal.decode_token(token)
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'raises a token error when the issuer is not valid' do
|
89
|
+
token = Sandal.encode_token({ 'iss' => 'example.org' }, nil)
|
90
|
+
expect { Sandal.decode_token(token) do |header, options|
|
91
|
+
options[:valid_iss] = ['example.net']
|
92
|
+
nil
|
93
|
+
end }.to raise_error Sandal::TokenError
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'does not raise an error when the issuer is valid' do
|
97
|
+
token = Sandal.encode_token({ 'iss' => 'example.org' }, nil)
|
98
|
+
Sandal.decode_token(token) do |header, options|
|
99
|
+
options[:valid_iss] = ['example.org', 'example.com']
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'raises a token error when the audience string is not valid' do
|
105
|
+
token = Sandal.encode_token({ 'aud' => 'example.com' }, nil)
|
106
|
+
expect { Sandal.decode_token(token) do |header, options|
|
107
|
+
options[:valid_aud] = ['example.net']
|
108
|
+
nil
|
109
|
+
end }.to raise_error Sandal::TokenError
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'raises a token error when the audience array is not valid' do
|
113
|
+
token = Sandal.encode_token({ 'aud' => ['example.org', 'example.com'] }, nil)
|
114
|
+
expect { Sandal.decode_token(token) do |header, options|
|
115
|
+
options[:valid_aud] = ['example.net']
|
116
|
+
nil
|
117
|
+
end }.to raise_error Sandal::TokenError
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'does not raise an error when the audience string is valid' do
|
121
|
+
token = Sandal.encode_token({ 'aud' => 'example.net' }, nil)
|
122
|
+
Sandal.decode_token(token) do |header, options|
|
123
|
+
options[:valid_aud] = ['example.net']
|
124
|
+
nil
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'does not raise an error when the audience array is valid' do
|
129
|
+
token = Sandal.encode_token({ 'aud' => ['example.com', 'example.net'] }, nil)
|
130
|
+
Sandal.decode_token(token) do |header, options|
|
131
|
+
options[:valid_aud] = ['example.net']
|
132
|
+
nil
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'encodes and decodes tokens with RS256 signatures' do
|
137
|
+
payload = 'Hello RSA256'
|
138
|
+
private_key = OpenSSL::PKey::RSA.generate(2048)
|
139
|
+
token = Sandal.encode_token(payload, Sandal::Sig::RS256.new(private_key))
|
140
|
+
decoded_payload = Sandal.decode_token(token) { |header| Sandal::Sig::RS256.new(private_key.public_key) }
|
141
|
+
decoded_payload.should == payload
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'encodes and decodes tokens with RS384 signatures' do
|
145
|
+
payload = 'Hello RSA384'
|
146
|
+
private_key = OpenSSL::PKey::RSA.generate(2048)
|
147
|
+
token = Sandal.encode_token(payload, Sandal::Sig::RS384.new(private_key))
|
148
|
+
decoded_payload = Sandal.decode_token(token) { |header| Sandal::Sig::RS384.new(private_key.public_key) }
|
149
|
+
decoded_payload.should == payload
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'encodes and decodes tokens with RS512 signatures' do
|
153
|
+
payload = 'Hello RSA512'
|
154
|
+
private_key = OpenSSL::PKey::RSA.generate(2048)
|
155
|
+
token = Sandal.encode_token(payload, Sandal::Sig::RS512.new(private_key))
|
156
|
+
decoded_payload = Sandal.decode_token(token) { |header| Sandal::Sig::RS512.new(private_key.public_key) }
|
157
|
+
decoded_payload.should == payload
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sandal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,26 +9,156 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-03-
|
13
|
-
dependencies:
|
12
|
+
date: 2013-03-30 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: json
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.7'
|
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.7'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: bundler
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '1.3'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '1.3'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rake
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '10.0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rspec
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '2.13'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '2.13'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: coveralls
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0.6'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0.6'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: yard
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ~>
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0.8'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0.8'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: redcarpet
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '2.2'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ~>
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '2.2'
|
14
126
|
description: A ruby library for creating and reading JSON Web Tokens (JWT), supporting
|
15
127
|
JSON Web Signatures (JWS) and JSON Web Encryption (JWE).
|
16
|
-
email:
|
17
|
-
- greg@gregbeech.com
|
128
|
+
email: greg@gregbeech.com
|
18
129
|
executables: []
|
19
130
|
extensions: []
|
20
|
-
extra_rdoc_files:
|
131
|
+
extra_rdoc_files:
|
132
|
+
- README.md
|
133
|
+
- LICENSE.md
|
21
134
|
files:
|
135
|
+
- .coveralls.yml
|
136
|
+
- .gitignore
|
137
|
+
- .rspec
|
138
|
+
- .travis.yml
|
139
|
+
- .yardopts
|
140
|
+
- CHANGELOG.md
|
141
|
+
- Gemfile
|
142
|
+
- LICENSE.md
|
143
|
+
- README.md
|
144
|
+
- Rakefile
|
145
|
+
- lib/sandal.rb
|
146
|
+
- lib/sandal/enc.rb
|
22
147
|
- lib/sandal/enc/aescbc.rb
|
23
148
|
- lib/sandal/enc/aesgcm.rb
|
24
|
-
- lib/sandal/
|
149
|
+
- lib/sandal/sig.rb
|
25
150
|
- lib/sandal/sig/es.rb
|
26
151
|
- lib/sandal/sig/hs.rb
|
27
152
|
- lib/sandal/sig/rs.rb
|
28
|
-
- lib/sandal/sig.rb
|
29
153
|
- lib/sandal/util.rb
|
30
154
|
- lib/sandal/version.rb
|
31
|
-
-
|
155
|
+
- sandal.gemspec
|
156
|
+
- spec/helper.rb
|
157
|
+
- spec/sandal/sig/es_spec.rb
|
158
|
+
- spec/sandal/sig/hs_spec.rb
|
159
|
+
- spec/sandal/sig/rs_spec.rb
|
160
|
+
- spec/sandal/util_spec.rb
|
161
|
+
- spec/sandal_spec.rb
|
32
162
|
homepage: http://rubygems.org/gems/sandal
|
33
163
|
licenses:
|
34
164
|
- MIT
|
@@ -42,17 +172,30 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
42
172
|
- - ! '>='
|
43
173
|
- !ruby/object:Gem::Version
|
44
174
|
version: '0'
|
175
|
+
segments:
|
176
|
+
- 0
|
177
|
+
hash: -3672992457221215645
|
45
178
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
179
|
none: false
|
47
180
|
requirements:
|
48
181
|
- - ! '>='
|
49
182
|
- !ruby/object:Gem::Version
|
50
183
|
version: '0'
|
51
|
-
|
184
|
+
segments:
|
185
|
+
- 0
|
186
|
+
hash: -3672992457221215645
|
187
|
+
requirements:
|
188
|
+
- openssl 1.0.1c for EC signature methods
|
52
189
|
rubyforge_project:
|
53
190
|
rubygems_version: 1.8.25
|
54
191
|
signing_key:
|
55
192
|
specification_version: 3
|
56
193
|
summary: A JSON Web Token (JWT) library.
|
57
|
-
test_files:
|
194
|
+
test_files:
|
195
|
+
- spec/helper.rb
|
196
|
+
- spec/sandal/sig/es_spec.rb
|
197
|
+
- spec/sandal/sig/hs_spec.rb
|
198
|
+
- spec/sandal/sig/rs_spec.rb
|
199
|
+
- spec/sandal/util_spec.rb
|
200
|
+
- spec/sandal_spec.rb
|
58
201
|
has_rdoc:
|