sandal 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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"