sandal 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 03610644805fbd043ba5d03d61a1d5926b45d37f
4
- data.tar.gz: 9622863e1097e93f74740cd112ae2dd4d98a8fd7
3
+ metadata.gz: 5ea79b1b4960ee68f883439155f86e455f840297
4
+ data.tar.gz: 88fe766c7e0dd6f42ffe8cc9e362bd4f9d4dcf1c
5
5
  SHA512:
6
- metadata.gz: ff2c734b2f07a064bad1701e26d976db390d9ebe2e666c41f066d80948486f7b16dca0ff189ba6772826dae589da2390195695fe3fa187e03b11cfc398325094
7
- data.tar.gz: 77ea15a46b9170678cc34382f690e283ee0b1ec448b7e1caa29edfdfe053e5b41ee9615278687dd711c90bfd4828b0922714a6acd1ecce5e98ee27dd518c41a8
6
+ metadata.gz: 8caf814bd10d413690942fde42d319a9b1d65afcef90c3ebf19f27f35b731e2d48c4d4225fb0700a60c9841c98ea46a6e34c610b40d93cf5584c605b6144621a
7
+ data.tar.gz: 978832491980e5e85b83bdac17e4c615cddc01594600785db0e7b7a621d459b7955003ab0352dac65ba803ded8890780cb609895528a6f35f60e107938dc8be6
@@ -2,6 +2,5 @@ language: ruby
2
2
  rvm:
3
3
  - rbx-19mode
4
4
  - jruby-19mode
5
- - 1.9.2
6
5
  - 1.9.3
7
6
  - 2.0.0
@@ -1,3 +1,17 @@
1
+ ## 0.5.0 (07 June 2013)
2
+
3
+ Features:
4
+
5
+ - Updated to JWT draft-08 specification, and corresponding JWE, JWS and JWA drafts.
6
+ - Added a KeyError class for when invalid keys are given to the library.
7
+ - Added an ExpiredTokenError class to make handling the common case of expired tokens easier.
8
+ - Added a NAME constant to all classes with a JWA name to save user having to hard-code the name string.
9
+
10
+ Breaking changes:
11
+
12
+ - Tokens are not backwards compatible with previous versions of the library due to changes in the specification.
13
+ - Dropped support for Ruby 1.9.2; supported platforms are now 1.9.3, 2.0.0, JRuby (head) and Rubinius (head).
14
+
1
15
  ## 0.4.0 (30 April 2013)
2
16
 
3
17
  Features:
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 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) [![Code Climate](https://codeclimate.com/github/gregbeech/sandal.png)](https://codeclimate.com/github/gregbeech/sandal) [![Dependency Status](https://gemnasium.com/gregbeech/sandal.png)](https://gemnasium.com/gregbeech/sandal)
2
2
 
3
- 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.
3
+ A Ruby library for creating and reading [JSON Web Tokens (JWT) draft-08](http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-08), supporting [JSON Web Signatures (JWS) draft-11](http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-11) and [JSON Web Encryption (JWE) draft-11](http://tools.ietf.org/html/draft-ietf-jose-json-web-encryption-11). See the [CHANGELOG](CHANGELOG.md) for version history.
4
4
 
5
5
  ## Installation
6
6
 
@@ -1,48 +1,60 @@
1
- require 'multi_json'
2
- require 'openssl'
3
- require 'zlib'
4
- require 'sandal/version'
5
- require 'sandal/claims'
6
- require 'sandal/enc'
7
- require 'sandal/sig'
8
- require 'sandal/util'
9
-
10
-
11
- # A library for creating and reading JSON Web Tokens (JWT), supporting JSON Web
12
- # Signatures (JWS) and JSON Web Encryption (JWE).
1
+ require "multi_json"
2
+ require "openssl"
3
+ require "zlib"
4
+ require "sandal/version"
5
+ require "sandal/claims"
6
+ require "sandal/enc"
7
+ require "sandal/sig"
8
+ require "sandal/util"
9
+
10
+
11
+ # A library for creating and reading JSON Web Tokens (JWT), supporting JSON Web Signatures (JWS) and JSON Web Encryption
12
+ # (JWE).
13
13
  #
14
- # Currently supports draft-06 of the JWT spec, and draft-08 of the JWS and JWE
15
- # specs.
14
+ # Currently supports draft-07 of the JWT spec, and draft-10 of the JWS and JWE specs.
16
15
  module Sandal
17
16
  extend Sandal::Util
18
17
 
18
+ # The base error for all errors raised by this library.
19
+ class Error < StandardError; end
20
+
21
+ # The error that is raised when a key provided for signing/encryption/etc. is invalid.
22
+ class KeyError < Error; end
23
+
24
+ # The error that is raised when there is a problem with a token.
25
+ class TokenError < Error; end
26
+
19
27
  # The error that is raised when a token is invalid.
20
- class TokenError < StandardError; end
28
+ class InvalidTokenError < TokenError; end
21
29
 
22
30
  # The error that is raised when a claim within a token is invalid.
23
- class ClaimError < TokenError; end
31
+ class ClaimError < InvalidTokenError; end
32
+
33
+ # The error that is raised when the token has expired.
34
+ class ExpiredTokenError < ClaimError; end
35
+
36
+ # The error that is raised when a token is unsupported (e.g. the algorithm used to encrypt the token is not supported
37
+ # by this library or by the Ruby platform it is executing on).
38
+ class UnsupportedTokenError < TokenError; end
24
39
 
25
40
  # The default options for token handling.
26
41
  #
27
42
  # ignore_exp::
28
- # Whether to ignore the expiry date of the token. This setting is just to
29
- # help get things working and should always be false in real apps!
43
+ # Whether to ignore the expiry date of the token. This setting is just to help get things working and should always
44
+ # be false in real apps!
30
45
  # ignore_nbf::
31
- # Whether to ignore the not-before date of the token. This setting is just
32
- # to help get things working and should always be false in real apps!
46
+ # Whether to ignore the not-before date of the token. This setting is just to help get things working and should
47
+ # always be false in real apps!
33
48
  # ignore_signature::
34
- # Whether to ignore the signature of signed (JWS) tokens. This setting is
35
- # just tohelp get things working and should always be false in real apps!
49
+ # Whether to ignore the signature of signed (JWS) tokens. This setting is just tohelp get things working and should
50
+ # always be false in real apps!
36
51
  # max_clock_skew::
37
- # The maximum clock skew, in seconds, when validating times. If your server
38
- # time is out of sync with the token server then this can be increased to
39
- # take that into account. It probably shouldn't be more than about 300.
52
+ # The maximum clock skew, in seconds, when validating times. If your server time is out of sync with the token
53
+ # server then this can be increased to take that into account. It probably shouldn't be more than about 300.
40
54
  # valid_iss::
41
- # A list of valid token issuers, if validation of the issuer claim is
42
- # required.
55
+ # A list of valid token issuers, if validation of the issuer claim is required.
43
56
  # valid_aud::
44
- # A list of valid audiences, if validation of the audience claim is
45
- # required.
57
+ # A list of valid audiences, if validation of the audience claim is required.
46
58
  DEFAULT_OPTIONS = {
47
59
  ignore_exp: false,
48
60
  ignore_nbf: false,
@@ -54,8 +66,7 @@ module Sandal
54
66
 
55
67
  # Overrides the default options.
56
68
  #
57
- # @param defaults [Hash] The options to override (see {DEFAULT_OPTIONS} for
58
- # details).
69
+ # @param defaults [Hash] The options to override (see {DEFAULT_OPTIONS} for details).
59
70
  # @return [Hash] The new default options.
60
71
  def self.default!(defaults)
61
72
  DEFAULT_OPTIONS.merge!(defaults)
@@ -67,7 +78,7 @@ module Sandal
67
78
  # @return [Boolean] true if the token is encrypted; otherwise false.
68
79
  def self.is_encrypted?(token)
69
80
  if token.is_a?(String)
70
- token.count('.') == 4
81
+ token.count(".") == 4
71
82
  else
72
83
  token.count == 5
73
84
  end
@@ -79,7 +90,7 @@ module Sandal
79
90
  # @return [Boolean] true if the token is signed; otherwise false.
80
91
  def self.is_signed?(token)
81
92
  if token.is_a?(String)
82
- !token.end_with?('.') && token.count('.') == 2
93
+ !token.end_with?(".") && token.count(".") == 2
83
94
  else
84
95
  token.count == 3 && !token[2].nil? && !token[2].empty?
85
96
  end
@@ -87,75 +98,65 @@ module Sandal
87
98
 
88
99
  # Creates a signed JSON Web Token.
89
100
  #
90
- # @param payload [String or Hash] The payload of the token. Hashes will be
91
- # encoded as JSON.
92
- # @param signer [#name,#sign] The token signer, which may be nil for an
93
- # unsigned token.
94
- # @param header_fields [Hash] Header fields for the token (note: do not
95
- # include 'alg').
101
+ # @param payload [String or Hash] The payload of the token. Hashes will be encoded as JSON.
102
+ # @param signer [#name,#sign] The token signer, which may be nil for an unsigned token.
103
+ # @param header_fields [Hash] Header fields for the token (note: do not include "alg").
96
104
  # @return [String] A signed JSON Web Token.
97
105
  def self.encode_token(payload, signer, header_fields = nil)
98
106
  signer ||= Sandal::Sig::NONE
99
107
 
100
108
  header = {}
101
- header['alg'] = signer.name
109
+ header["alg"] = signer.name
102
110
  header = header_fields.merge(header) if header_fields
103
111
  header = MultiJson.dump(header)
104
112
 
105
113
  payload = MultiJson.dump(payload) unless payload.is_a?(String)
106
114
 
107
- sec_input = [header, payload].map { |p| jwt_base64_encode(p) }.join('.')
115
+ sec_input = [header, payload].map { |p| jwt_base64_encode(p) }.join(".")
108
116
  signature = signer.sign(sec_input)
109
- [sec_input, jwt_base64_encode(signature)].join('.')
117
+ [sec_input, jwt_base64_encode(signature)].join(".")
110
118
  end
111
119
 
112
120
  # Creates an encrypted JSON Web Token.
113
121
  #
114
122
  # @param payload [String] The payload of the token.
115
123
  # @param encrypter [#name,#alg,#encrypt] The token encrypter.
116
- # @param header_fields [Hash] Header fields for the token (note: do not
117
- # include 'alg' or 'enc').
124
+ # @param header_fields [Hash] Header fields for the token (note: do not include "alg" or "enc").
118
125
  # @return [String] An encrypted JSON Web Token.
119
126
  def self.encrypt_token(payload, encrypter, header_fields = nil)
120
127
  header = {}
121
- header['enc'] = encrypter.name
122
- header['alg'] = encrypter.alg.name
128
+ header["enc"] = encrypter.name
129
+ header["alg"] = encrypter.alg.name
123
130
  header = header_fields.merge(header) if header_fields
124
131
 
125
- if header.has_key?('zip')
126
- unless header['zip'] == 'DEF'
127
- raise ArgumentError, 'Invalid zip algorithm.'
132
+ if header.has_key?("zip")
133
+ unless header["zip"] == "DEF"
134
+ raise ArgumentError, "Invalid zip algorithm."
128
135
  end
129
136
  payload = Zlib::Deflate.deflate(payload, Zlib::BEST_COMPRESSION)
130
137
  end
131
138
 
132
- encrypter.encrypt(header, payload)
139
+ encrypter.encrypt(MultiJson.dump(header), payload)
133
140
  end
134
141
 
135
- # Decodes and validates a signed and/or encrypted JSON Web Token, recursing
136
- # into any nested tokens, and returns the payload.
142
+ # Decodes and validates a signed and/or encrypted JSON Web Token, recursing into any nested tokens, and returns the
143
+ # payload.
137
144
  #
138
- # The block is called with the token header as the first parameter, and should
139
- # return the appropriate signature or decryption method to either validate the
140
- # signature or decrypt the token as applicable. When the tokens are nested,
141
- # this block will be called once per token. It can optionally have a second
142
- # options parameter which can be used to override the {DEFAULT_OPTIONS} on a
143
- # per-token basis; options are not persisted between yields.
145
+ # The block is called with the token header as the first parameter, and should return the appropriate signature or
146
+ # decryption method to either validate the signature or decrypt the token as applicable. When the tokens are nested,
147
+ # this block will be called once per token. It can optionally have a second options parameter which can be used to
148
+ # override the {DEFAULT_OPTIONS} on a per-token basis; options are not persisted between yields.
144
149
  #
145
150
  # @param token [String] The encoded JSON Web Token.
146
151
  # @param depth [Integer] The maximum depth of token nesting to decode to.
147
152
  # @yieldparam header [Hash] The JWT header values.
148
- # @yieldparam options [Hash] (Optional) A hash that can be used to override
149
- # the default options.
150
- # @yieldreturn [#valid? or #decrypt] The signature validator if the token is
151
- # signed, or the token decrypter if the token is encrypted.
152
- # @return [Hash or String] The payload of the token as a Hash if it was JSON,
153
- # otherwise as a String.
154
- # @raise [Sandal::ClaimError] One or more claims in the token is invalid.
155
- # @raise [Sandal::TokenError] The token format is invalid, or validation of
156
- # the token failed.
153
+ # @yieldparam options [Hash] (Optional) A hash that can be used to override the default options.
154
+ # @yieldreturn [#valid? or #decrypt] The signature validator if the token is signed, or the token decrypter if the
155
+ # token is encrypted.
156
+ # @return [Hash or String] The payload of the token as a Hash if it was JSON, otherwise as a String.
157
+ # @raise [Sandal::TokenError] The token is invalid or not supported.
157
158
  def self.decode_token(token, depth = 16)
158
- parts = token.split('.')
159
+ parts = token.split(".")
159
160
  decoded_parts = decode_token_parts(parts)
160
161
  header = decoded_parts[0]
161
162
 
@@ -163,10 +164,10 @@ module Sandal
163
164
  decoder = yield header, options if block_given?
164
165
 
165
166
  if is_encrypted?(parts)
166
- payload = decoder.decrypt(parts, decoded_parts)
167
- if header.has_key?('zip')
168
- unless header['zip'] == 'DEF'
169
- raise Sandal::TokenError, 'Invalid zip algorithm.'
167
+ payload = decoder.decrypt(parts)
168
+ if header.has_key?("zip")
169
+ unless header["zip"] == "DEF"
170
+ raise Sandal::InvalidTokenError, "Invalid zip algorithm."
170
171
  end
171
172
  payload = Zlib::Inflate.inflate(payload)
172
173
  end
@@ -177,7 +178,7 @@ module Sandal
177
178
  end
178
179
  end
179
180
 
180
- if header['cty'] == 'JWT'
181
+ if header["cty"] == "JWT"
181
182
  if depth > 0
182
183
  if block_given?
183
184
  decode_token(payload, depth - 1, &Proc.new)
@@ -197,9 +198,9 @@ module Sandal
197
198
  # Decodes and validates a signed JSON Web Token.
198
199
  def self.validate_signature(parts, signature, validator)
199
200
  validator ||= Sandal::Sig::NONE
200
- secured_input = parts.take(2).join('.')
201
+ secured_input = parts.take(2).join(".")
201
202
  unless validator.valid?(signature, secured_input)
202
- raise TokenError, 'Invalid signature.'
203
+ raise TokenError, "Invalid signature."
203
204
  end
204
205
  end
205
206
 
@@ -209,7 +210,7 @@ module Sandal
209
210
  parts[0] = MultiJson.load(parts[0])
210
211
  parts
211
212
  rescue
212
- raise TokenError, 'Invalid token encoding.'
213
+ raise TokenError, "Invalid token encoding."
213
214
  end
214
215
 
215
216
  # Parses the content of a token and validates the claims if is JSON claims.
@@ -21,14 +21,14 @@ module Sandal
21
21
  #
22
22
  # @param max_clock_skew [Numeric] The maximum clock skew, in seconds.
23
23
  # @return [void].
24
- # @raise [Sandal::ClaimError] The 'exp' claim is invalid, or the token has
24
+ # @raise [Sandal::ClaimError] The "exp" claim is invalid, or the token has
25
25
  # expired.
26
26
  def validate_exp(max_clock_skew = 0)
27
27
  max_clock_skew ||= 0
28
28
 
29
- exp = time_claim('exp')
29
+ exp = time_claim("exp")
30
30
  if exp && exp <= (Time.now - max_clock_skew)
31
- raise Sandal::ClaimError, 'The token has expired.'
31
+ raise Sandal::ExpiredTokenError, "The token has expired."
32
32
  end
33
33
  end
34
34
 
@@ -36,14 +36,14 @@ module Sandal
36
36
  #
37
37
  # @param max_clock_skew [Numeric] The maximum clock skew, in seconds.
38
38
  # @return [void].
39
- # @raise [Sandal::ClaimError] The 'nbf' claim is invalid, or the token is
39
+ # @raise [Sandal::ClaimError] The "nbf" claim is invalid, or the token is
40
40
  # not valid yet.
41
41
  def validate_nbf(max_clock_skew = 0)
42
42
  max_clock_skew ||= 0
43
43
 
44
- nbf = time_claim('nbf')
44
+ nbf = time_claim("nbf")
45
45
  if nbf && nbf > (Time.now + max_clock_skew)
46
- raise Sandal::ClaimError, 'The token is not valid yet.'
46
+ raise Sandal::ClaimError, "The token is not valid yet."
47
47
  end
48
48
  end
49
49
 
@@ -51,12 +51,12 @@ module Sandal
51
51
  #
52
52
  # @param valid_iss [Array] The valid issuers.
53
53
  # @return [void].
54
- # @raise [Sandal::ClaimError] The 'iss' claim value is not a valid issuer.
54
+ # @raise [Sandal::ClaimError] The "iss" claim value is not a valid issuer.
55
55
  def validate_iss(valid_iss)
56
56
  return unless valid_iss && valid_iss.length > 0
57
57
 
58
- unless valid_iss.include?(self['iss'])
59
- raise Sandal::ClaimError, 'The issuer is invalid.'
58
+ unless valid_iss.include?(self["iss"])
59
+ raise Sandal::ClaimError, "The issuer is invalid."
60
60
  end
61
61
  end
62
62
 
@@ -64,15 +64,15 @@ module Sandal
64
64
  #
65
65
  # @param valid_aud [Array] The valid audiences.
66
66
  # @return [void].
67
- # @raise [Sandal::ClaimError] The 'aud' claim value does not contain a valid
67
+ # @raise [Sandal::ClaimError] The "aud" claim value does not contain a valid
68
68
  # audience.
69
69
  def validate_aud(valid_aud)
70
70
  return unless valid_aud && valid_aud.length > 0
71
71
 
72
- aud = self['aud']
72
+ aud = self["aud"]
73
73
  aud = [aud] unless aud.is_a?(Array)
74
74
  unless (aud & valid_aud).length > 0
75
- raise Sandal::ClaimError, 'The audence is invalid.'
75
+ raise Sandal::ClaimError, "The audence is invalid."
76
76
  end
77
77
  end
78
78
 
@@ -85,7 +85,7 @@ module Sandal
85
85
  begin
86
86
  Time.at(claim)
87
87
  rescue
88
- raise Sandal::ClaimError, "The '#{name}' claim is invalid."
88
+ raise Sandal::ClaimError, "The "#{name}" claim is invalid."
89
89
  end
90
90
  end
91
91
  end
@@ -1,60 +1,26 @@
1
- require 'openssl'
1
+ require "openssl"
2
+ require "sandal/util"
2
3
 
3
4
  module Sandal
4
5
  # Contains encryption (JWE) functionality.
5
6
  module Enc
6
7
 
7
- # The Concat Key Derivation Function.
8
+ # Gets the decoded parts of a JWE token.
8
9
  #
9
- # @param digest [OpenSSL::Digest or String] The digest for the algorithm.
10
- # @param key [String] The key or shared secret.
11
- # @param keydatalen [Integer] The desired output size in bits.
12
- # @param algorithm_id [String] The name of the algorithm.
13
- # @param party_u_info [String or 0] The partyUInfo.
14
- # @param party_v_info [String or 0] The partyVInfo.
15
- # @param supp_pub_info [String] Supplementary public info.
16
- # @param supp_priv_info [String] Supplementary private info.
17
- # @return [String] The derived keying material.
18
- def self.concat_kdf(digest, key, keydatalen, algorithm_id,
19
- party_u_info, party_v_info,
20
- supp_pub_info = nil, supp_priv_info = nil)
21
- digest = OpenSSL::Digest.new(digest) if digest.is_a?(String)
22
- rounds = (keydatalen / (digest.digest_length * 8.0)).ceil
23
-
24
- round_input = concat_kdf_round_input(key, keydatalen, algorithm_id,
25
- party_u_info, party_v_info,
26
- supp_pub_info, supp_priv_info)
27
-
28
- (1..rounds).reduce('') do |output, round|
29
- hash = digest.digest([round].pack('N') + round_input)
30
- if round == rounds
31
- round_bits = keydatalen % (digest.digest_length * 8)
32
- hash = hash[0...(round_bits / 8)] unless round_bits == 0
33
- end
34
- output << hash
35
- end
36
- end
37
-
38
- private
39
-
40
- # The round input for the Concat KDF function (excluding round number).
41
- def self.concat_kdf_round_input(key, keydatalen, algorithm_id,
42
- party_u_info, party_v_info,
43
- supp_pub_info, supp_priv_info)
44
- input = ''.force_encoding('binary')
45
- input << key.force_encoding('binary')
46
- input << [keydatalen].pack('N')
47
- input << algorithm_id.force_encoding('binary')
48
- input << (party_u_info == 0 ? [0].pack('N') : party_u_info.force_encoding('binary'))
49
- input << (party_v_info == 0 ? [0].pack('N') : party_v_info.force_encoding('binary'))
50
- input << supp_pub_info.force_encoding('binary') if supp_pub_info
51
- input << supp_priv_info.force_encoding('binary') if supp_priv_info
52
- input
10
+ # @param token [String or Array] The token, or encoded token parts.
11
+ # @return [[Array, Array]] The encoded parts and the decoded parts.
12
+ def self.token_parts(token)
13
+ parts = token.is_a?(Array) ? token : token.split(".")
14
+ raise ArgumentError unless parts.length == 5
15
+ decoded_parts = parts.map { |part| jwt_base64_decode(part) }
16
+ return parts, decoded_parts
17
+ rescue ArgumentError
18
+ raise Sandal::InvalidTokenError, "Invalid token encoding."
53
19
  end
54
20
 
55
21
  end
56
22
  end
57
23
 
58
- require 'sandal/enc/acbc_hs'
59
- require 'sandal/enc/agcm' unless RUBY_VERSION < '2.0.0'
60
- require 'sandal/enc/alg'
24
+ require "sandal/enc/acbc_hs"
25
+ require "sandal/enc/agcm" unless RUBY_VERSION < "2.0.0"
26
+ require "sandal/enc/alg"