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 ADDED
@@ -0,0 +1 @@
1
+ service_name: travis-ci
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .DS_Store
6
+ coverage
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ lib/bundler/man
10
+ pkg
11
+ rdoc
12
+ spec/reports
13
+ test/tmp
14
+ test/version_tmp
15
+ tmp
16
+
17
+ # YARD artifacts
18
+ .yardoc
19
+ _yardoc
20
+ doc/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - rbx-19mode
4
+ - jruby-19mode
5
+ - 1.9.2
6
+ - 1.9.3
7
+ - 2.0.0
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --no-private
2
+ --protected
3
+ lib/**/*.rb
4
+ -
5
+ README.md
6
+ LICENSE.md
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ ## 0.1.0 (30 March 2013)
2
+
3
+ The first version worth using.
4
+
5
+ Features:
6
+
7
+ - Supports all JWA signature algorithms (ES, HS, RS, none).
8
+ - Validates exp, nbf, iss and aud claims.
9
+ - Configurable token validation options.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
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
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task :default => :spec
7
+ task :build => :spec
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
- throw ArgumentError.new('The header cannot contain an "enc" parameter.')
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 encrypted [Sandal::Enc] The token encrypter.
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, verifying the signature as necessary.
86
+ # Decodes and validates a JSON Web Token.
55
87
  #
56
- # **NOTE: This method is likely to change, to allow more validation options**
57
- def self.decode_token(token, &sig_finder)
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
- throw ArgumentError.new('Invalid token format.') unless [2, 3].include?(parts.length)
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
- throw ArgumentError.new('Invalid token encoding.')
108
+ raise TokenError, 'Invalid token encoding.'
66
109
  end
67
110
 
68
- algorithm = header['alg']
69
- if algorithm && algorithm != 'none'
70
- throw SecurityError.new('The signature is missing.') unless signature.length > 0
71
- sig = sig_finder.call(header)
72
- throw SecurityError.new('No signature verifier was found.') unless sig
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
- throw ArgumentError.new('Invalid signature.') unless sig.verify(signature, secured_input)
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
- throw ArgumentError.new('Invalid token format.') unless parts.length == 5
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
- throw ArgumentError.new('Invalid token encoding.')
145
+ raise ArgumentError, 'Invalid token encoding.'
94
146
  end
95
147
 
96
148
  enc = enc_finder.call(header)
97
- throw SecurityError.new('No decryptor was found.') unless enc
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
- end
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
- puts claims.to_s
116
-
117
- # sign and encrypt
118
- jws_key = OpenSSL::PKey::RSA.new(2048)
119
- sig = Sandal::Sig::RS256.new(jws_key)
120
- jws_token = Sandal.encode_token(claims.to_s, sig)
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
- jwe_key = OpenSSL::PKey::RSA.new(2048)
125
- enc = Sandal::Enc::AES128GCM.new(jwe_key.public_key)
126
- jwe_token = Sandal.encrypt_token(jws_token, enc, { 'cty' => 'JWT' })
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
- puts jwe_token
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
- jws_token_2 = Sandal.decrypt_token(jwe_token) { |header| Sandal::Enc::AES128CBC.new(jwe_key) }
131
- roundtrip_claims = Sandal.decode_token(jws_token_2) { |header| Sandal::Sig::RS256.new(jws_key.public_key) }
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
- puts roundtrip_claims
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
- throw NotImplementedError.new("#{@name}.encrypt is not implemented.")
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
- throw NotImplementedError.new("#{@name}.decrypt is not implemented.")
18
+ raise NotImplementedError, "#{@name}.decrypt is not implemented."
19
19
  end
20
20
 
21
21
  end
@@ -9,7 +9,7 @@ module Sandal
9
9
  include Sandal::Enc
10
10
 
11
11
  def initialize(aes_size, key)
12
- throw ArgumentError.new('A key is required.') unless key
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
- throw ArgumentError.new('Invalid signature.') unless integrity_value == computed_integrity_value
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)
@@ -9,15 +9,15 @@ module Sandal
9
9
  include Sandal::Enc
10
10
 
11
11
  def initialize(aes_size, key)
12
- throw NotImplementedException.new('AES-CGM is not yet implemented.')
12
+ raise NotImplementedException, 'AES-CGM is not yet implemented.'
13
13
  end
14
14
 
15
15
  def encrypt(header, payload)
16
- throw NotImplementedException.new('AES-CGM is not yet implemented.')
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
- throw NotImplementedException.new('AES-CGM is not yet implemented.')
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
- throw NotImplementedError.new("#{@name}.sign is not implemented.")
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
- throw NotImplementedError.new("#{@name}.verify is not implemented.")
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
- throw ArgumentError.new('A key is required.') unless key
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
- throw ArgumentError.new('A key is required.') unless key
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
- throw ArgumentError.new('A key is required.') unless key
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
- throw ArgumentError.new('A private key is required to sign the payload.') unless @key.private?
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
 
@@ -1,4 +1,4 @@
1
1
  module Sandal
2
2
  # The semantic version of the library.
3
- VERSION = '0.0.3'
3
+ VERSION = '0.1.0'
4
4
  end
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,8 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
4
+ require 'rspec'
5
+ require "#{File.dirname(__FILE__)}/../lib/sandal.rb"
6
+
7
+ RSpec.configure do |c|
8
+ end
@@ -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
@@ -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.3
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-29 00:00:00.000000000 Z
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/enc.rb
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
- - lib/sandal.rb
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
- requirements: []
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: