sandal 0.1.0 → 0.1.1
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 +15 -0
- data/.gitignore +3 -4
- data/CHANGELOG.md +12 -0
- data/README.md +2 -2
- data/lib/sandal/claims.rb +82 -0
- data/lib/sandal/enc/aescbc.rb +1 -1
- data/lib/sandal/sig/es.rb +64 -13
- data/lib/sandal/sig/hs.rb +25 -8
- data/lib/sandal/sig/rs.rb +29 -12
- data/lib/sandal/sig.rb +8 -26
- data/lib/sandal/util.rb +8 -0
- data/lib/sandal/version.rb +1 -1
- data/lib/sandal.rb +40 -83
- data/sandal.gemspec +8 -8
- data/spec/sandal/sig/es_spec.rb +32 -14
- data/spec/sandal/sig/hs_spec.rb +3 -3
- data/spec/sandal/sig/rs_spec.rb +6 -6
- data/spec/sandal/util_spec.rb +4 -0
- data/spec/sandal_spec.rb +16 -16
- metadata +19 -40
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
N2E4NjhhOTY5Y2JkNGU1MmU1YjE2MmIzYTY4NzUwMTg5ZTc3NTk0OA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
YjA1YTA1MzExMDg2YmMwNDllNWQyYTgxZjNmMmQ0MjJhNWNiNTM0Mw==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
YTBlODVkNGFiNWQwNGQzZWE2YjFkY2JmNTg4MDliMDlkN2YyYjA0NzBmYWYw
|
10
|
+
OTk5MjMzMjViYjNiODBjOGU0YmQ5MjlmMzUxODI2MzU4NzU5ZTI3ODQ1ZTk5
|
11
|
+
YmI1OThiMDFkZDNjNzRlMmViZTUxYjU5MWU0M2MwMGE4NDcxMDg=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZWFhNjNlNzc4NDgwNmI5OTAwMmNhNTFlNDE1NjhjY2ViM2E2N2FlMjQ5NjM3
|
14
|
+
MDkxZDk5MmFiMzY2NGM4YTlkMjY0MzI5NDI1YTRlYWIzOWY1ZTA2MzE2NzMw
|
15
|
+
NTIwMjA1ZjQxMTQ1ZWU3Y2I2YjA3YmI5NDA3OTgxODZhMGRkMjA=
|
data/.gitignore
CHANGED
@@ -2,19 +2,18 @@
|
|
2
2
|
*.rbc
|
3
3
|
.bundle
|
4
4
|
.config
|
5
|
-
.DS_Store
|
6
5
|
coverage
|
6
|
+
doc
|
7
|
+
.DS_Store
|
7
8
|
Gemfile.lock
|
8
9
|
InstalledFiles
|
9
10
|
lib/bundler/man
|
10
11
|
pkg
|
12
|
+
.rbx
|
11
13
|
rdoc
|
12
14
|
spec/reports
|
13
15
|
test/tmp
|
14
16
|
test/version_tmp
|
15
17
|
tmp
|
16
|
-
|
17
|
-
# YARD artifacts
|
18
18
|
.yardoc
|
19
19
|
_yardoc
|
20
|
-
doc/
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
## 0.1.1 (01 April 2013)
|
2
|
+
|
3
|
+
Features:
|
4
|
+
|
5
|
+
- Changed from json to multi_json gem for improved compatibility.
|
6
|
+
- New Claims module can add claims validation functionality to Hash-like objects.
|
7
|
+
- New ClaimError type for claim validation errors.
|
8
|
+
|
9
|
+
Bug fixes:
|
10
|
+
|
11
|
+
- Base64 decoding now ensures there is no padding before decoding.
|
12
|
+
|
1
13
|
## 0.1.0 (30 March 2013)
|
2
14
|
|
3
15
|
The first version worth using.
|
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
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
2
|
|
3
|
-
# Sandal [](https://travis-ci.org/gregbeech/sandal) [](https://coveralls.io/r/gregbeech/sandal)
|
3
|
+
# Sandal [](https://travis-ci.org/gregbeech/sandal) [](https://coveralls.io/r/gregbeech/sandal) [](https://codeclimate.com/github/gregbeech/sandal)
|
4
4
|
|
5
|
-
A Ruby library for creating and reading [JSON Web Tokens (JWT)
|
5
|
+
A Ruby library for creating and reading [JSON Web Tokens (JWT) drfat-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
6
|
|
7
7
|
## Installation
|
8
8
|
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Sandal
|
2
|
+
# A module that can be mixed into Hash-like objects to provide claims-related functionality.
|
3
|
+
module Claims
|
4
|
+
|
5
|
+
# Validates the set of claims.
|
6
|
+
#
|
7
|
+
# @param options [Hash] The validation options (see {Sandal::DEFAULT_OPTIONS} for details).
|
8
|
+
# @return [Hash] A reference to self.
|
9
|
+
# @raise [Sandal::ClaimError] One or more claims is invalid.
|
10
|
+
def validate_claims(options)
|
11
|
+
validate_exp(options[:max_clock_skew]) if options[:validate_exp]
|
12
|
+
validate_nbf(options[:max_clock_skew]) if options[:validate_nbf]
|
13
|
+
validate_iss(options[:valid_iss])
|
14
|
+
validate_aud(options[:valid_aud])
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
# Validates the expires claim.
|
19
|
+
#
|
20
|
+
# @param max_clock_skew [Numeric] The maximum clock skew, in seconds.
|
21
|
+
# @return [void].
|
22
|
+
# @raise [Sandal::ClaimError] The 'exp' claim is invalid, or the token has expired.
|
23
|
+
def validate_exp(max_clock_skew)
|
24
|
+
exp = time_claim('exp')
|
25
|
+
if exp && exp <= (Time.now - max_clock_skew)
|
26
|
+
raise Sandal::ClaimError, 'The token has expired.'
|
27
|
+
end
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
# Validates the not-before claim.
|
32
|
+
#
|
33
|
+
# @param max_clock_skew [Numeric] The maximum clock skew, in seconds.
|
34
|
+
# @return [void].
|
35
|
+
# @raise [Sandal::ClaimError] The 'nbf' claim is invalid, or the token is not valid yet.
|
36
|
+
def validate_nbf(max_clock_skew)
|
37
|
+
nbf = time_claim('nbf')
|
38
|
+
if nbf && nbf > (Time.now + max_clock_skew)
|
39
|
+
raise Sandal::ClaimError, 'The token is not valid yet.'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Validates the issuer claim.
|
44
|
+
#
|
45
|
+
# @param valid_iss [Array] The valid issuers.
|
46
|
+
# @return [void].
|
47
|
+
# @raise [Sandal::ClaimError] The 'iss' claim value is not a valid issuer.
|
48
|
+
def validate_iss(valid_iss)
|
49
|
+
if valid_iss && valid_iss.length > 0
|
50
|
+
raise Sandal::ClaimError, 'The issuer is invalid.' unless valid_iss.include?(self['iss'])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Validates the audience claim.
|
55
|
+
#
|
56
|
+
# @param valid_aud [Array] The valid audiences.
|
57
|
+
# @return [void].
|
58
|
+
# @raise [Sandal::ClaimError] The 'aud' claim value does not contain a valid audience.
|
59
|
+
def validate_aud(valid_aud)
|
60
|
+
if valid_aud && valid_aud.length > 0
|
61
|
+
aud = self['aud']
|
62
|
+
aud = [aud] unless aud.is_a?(Array)
|
63
|
+
raise Sandal::ClaimError, 'The audence is invalid.' unless (aud & valid_aud).length > 0
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# Gets the value of a claim as a Time.
|
70
|
+
def time_claim(name)
|
71
|
+
claim = self[name]
|
72
|
+
if claim
|
73
|
+
begin
|
74
|
+
Time.at(claim)
|
75
|
+
rescue
|
76
|
+
raise ClaimError, "The '#{name}' claim is invalid."
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
data/lib/sandal/enc/aescbc.rb
CHANGED
@@ -35,7 +35,7 @@ module Sandal
|
|
35
35
|
ciphertext = cipher.update(payload) + cipher.final
|
36
36
|
encoded_ciphertext = Sandal::Util.base64_encode(ciphertext)
|
37
37
|
|
38
|
-
encoded_header = Sandal::Util.base64_encode(
|
38
|
+
encoded_header = Sandal::Util.base64_encode(MultiJson.dump(header))
|
39
39
|
secured_input = [encoded_header, encoded_encrypted_key, encoded_iv, encoded_ciphertext].join('.')
|
40
40
|
content_integrity_key = derive_content_key('Integrity', content_master_key, @sha_size)
|
41
41
|
integrity_value = OpenSSL::HMAC.digest(@digest, content_integrity_key, secured_input)
|
data/lib/sandal/sig/es.rb
CHANGED
@@ -5,11 +5,16 @@ module Sandal
|
|
5
5
|
|
6
6
|
# Base implementation of the ECDSA-SHA family of signature algorithms.
|
7
7
|
class ES
|
8
|
-
include Sandal::Sig
|
9
8
|
|
10
|
-
#
|
9
|
+
# @return [String] The JWA name of the algorithm.
|
10
|
+
attr_reader :name
|
11
|
+
|
12
|
+
# Creates a new instance; it's probably easier to use one of the subclass constructors.
|
13
|
+
#
|
14
|
+
# @param sha_size [Integer] The size of the SHA algorithm.
|
15
|
+
# @param prime_size [Integer] The size of the ECDSA primes.
|
16
|
+
# @param key [OpenSSL::PKey::EC] The key to use for signing (private) or validation (public).
|
11
17
|
def initialize(sha_size, prime_size, key)
|
12
|
-
raise ArgumentError, 'A key is required.' unless key
|
13
18
|
@name = "ES#{sha_size}"
|
14
19
|
@digest = OpenSSL::Digest.new("sha#{sha_size}")
|
15
20
|
@prime_size = prime_size
|
@@ -17,6 +22,9 @@ module Sandal
|
|
17
22
|
end
|
18
23
|
|
19
24
|
# Signs a payload and returns the signature.
|
25
|
+
#
|
26
|
+
# @param payload [String] The payload of the token to sign.
|
27
|
+
# @return [String] The signature.
|
20
28
|
def sign(payload)
|
21
29
|
hash = @digest.digest(payload)
|
22
30
|
asn1_sig = @key.dsa_sign_asn1(hash)
|
@@ -24,64 +32,107 @@ module Sandal
|
|
24
32
|
self.class.encode_jws_signature(r, s, @prime_size)
|
25
33
|
end
|
26
34
|
|
27
|
-
#
|
28
|
-
|
35
|
+
# Validates a payload signature and returns whether the signature matches.
|
36
|
+
#
|
37
|
+
# @param signature [String] The signature to verify.
|
38
|
+
# @param payload [String] The payload of the token.
|
39
|
+
# @return [Boolean] true if the signature is correct; otherwise false.
|
40
|
+
def valid?(signature, payload)
|
29
41
|
hash = @digest.digest(payload)
|
30
42
|
r, s = self.class.decode_jws_signature(signature)
|
31
43
|
asn1_sig = self.class.encode_asn1_signature(r, s)
|
32
|
-
|
44
|
+
@key.dsa_verify_asn1(hash, asn1_sig)
|
33
45
|
end
|
34
46
|
|
35
|
-
# Decodes an
|
47
|
+
# Decodes an ASN.1 signature into a pair of BNs.
|
48
|
+
#
|
49
|
+
# @param signature [String] The ASN.1 signature.
|
50
|
+
# @return [OpenSSL::BN, OpenSSL::BN] A pair of BNs.
|
36
51
|
def self.decode_asn1_signature(signature)
|
37
52
|
asn_seq = OpenSSL::ASN1.decode(signature)
|
38
53
|
return asn_seq.value[0].value, asn_seq.value[1].value
|
39
54
|
end
|
40
55
|
|
41
|
-
# Encodes a pair of BNs into an
|
56
|
+
# Encodes a pair of BNs into an ASN.1 signature.
|
57
|
+
#
|
58
|
+
# @param r [OpenSSL::BN] The 'r' value.
|
59
|
+
# @param s [OpenSSL::BN] The 's' value.
|
60
|
+
# @return [String] The ASN.1 signature.
|
42
61
|
def self.encode_asn1_signature(r, s)
|
43
62
|
items = [OpenSSL::ASN1::Integer.new(r), OpenSSL::ASN1::Integer.new(s)]
|
44
63
|
OpenSSL::ASN1::Sequence.new(items).to_der
|
45
64
|
end
|
46
65
|
|
47
66
|
# Decodes a JWS signature into a pair of BNs.
|
67
|
+
#
|
68
|
+
# @param signature [String] The ASN.1 signature.
|
69
|
+
# @return [OpenSSL::BN, OpenSSL::BN] A pair of BNs.
|
48
70
|
def self.decode_jws_signature(signature)
|
49
71
|
n_length = signature.length / 2
|
50
|
-
s_to_n =
|
72
|
+
s_to_n = -> s { OpenSSL::BN.new(s.unpack('H*')[0], 16) }
|
51
73
|
r = s_to_n.call(signature[0..(n_length - 1)])
|
52
74
|
s = s_to_n.call(signature[n_length..-1])
|
53
75
|
return r, s
|
54
76
|
end
|
55
77
|
|
56
78
|
# Encodes a pair of BNs into a JWS signature.
|
79
|
+
#
|
80
|
+
# @param r [OpenSSL::BN] The 'r' value.
|
81
|
+
# @param s [OpenSSL::BN] The 's' value.
|
82
|
+
# @param prime_size [Integer] The size of the ECDSA primes.
|
83
|
+
# @return [String] The ASN.1 signature.
|
57
84
|
def self.encode_jws_signature(r, s, prime_size)
|
58
85
|
byte_count = (prime_size / 8.0).ceil
|
59
|
-
n_to_s =
|
86
|
+
n_to_s = -> n { [n.to_s(16)].pack('H*').rjust(byte_count, "\0") }
|
60
87
|
n_to_s.call(r) + n_to_s.call(s)
|
61
88
|
end
|
62
89
|
|
90
|
+
protected
|
91
|
+
|
92
|
+
# Ensures that a key has a specified curve name.
|
93
|
+
#
|
94
|
+
# @param key [OpenSSL::PKey::EC] The key.
|
95
|
+
# @param curve_name [String] The curve name.
|
96
|
+
# @return [void].
|
97
|
+
# @raise [ArgumentError] The key has a different curve name.
|
98
|
+
def ensure_curve(key, curve_name)
|
99
|
+
raise ArgumentError, "The key must be in the #{curve_name} group." unless key.group.curve_name == curve_name
|
100
|
+
end
|
101
|
+
|
63
102
|
end
|
64
103
|
|
65
104
|
# The ECDSA-SHA256 signing algorithm.
|
66
105
|
class ES256 < Sandal::Sig::ES
|
67
|
-
# Creates a new instance
|
106
|
+
# Creates a new instance.
|
107
|
+
#
|
108
|
+
# @param key [OpenSSL::PKey::EC] The key to use for signing (private) or validation (public).
|
109
|
+
# @raise [ArgumentError] The key is not in the "prime256v1" group.
|
68
110
|
def initialize(key)
|
111
|
+
ensure_curve(key, 'prime256v1')
|
69
112
|
super(256, 256, key)
|
70
113
|
end
|
71
114
|
end
|
72
115
|
|
73
116
|
# The ECDSA-SHA384 signing algorithm.
|
74
117
|
class ES384 < Sandal::Sig::ES
|
75
|
-
# Creates a new instance
|
118
|
+
# Creates a new instance.
|
119
|
+
#
|
120
|
+
# @param key [OpenSSL::PKey::EC] The key to use for signing (private) or validation (public).
|
121
|
+
# @raise [ArgumentError] The key is not in the "secp384r1" group.
|
76
122
|
def initialize(key)
|
123
|
+
ensure_curve(key, 'secp384r1')
|
77
124
|
super(384, 384, key)
|
78
125
|
end
|
79
126
|
end
|
80
127
|
|
81
128
|
# The ECDSA-SHA512 signing algorithm.
|
82
129
|
class ES512 < Sandal::Sig::ES
|
83
|
-
# Creates a new instance
|
130
|
+
# Creates a new instance.
|
131
|
+
#
|
132
|
+
# @param key [OpenSSL::PKey::EC] The key to use for signing (private) or validation (public).
|
133
|
+
# @raise [ArgumentError] The key is not in the "secp521r1" group.
|
84
134
|
def initialize(key)
|
135
|
+
ensure_curve(key, 'secp521r1')
|
85
136
|
super(512, 521, key)
|
86
137
|
end
|
87
138
|
end
|
data/lib/sandal/sig/hs.rb
CHANGED
@@ -5,23 +5,34 @@ module Sandal
|
|
5
5
|
|
6
6
|
# Base implementation of the HMAC-SHA family of signature algorithms.
|
7
7
|
class HS
|
8
|
-
include Sandal::Sig
|
9
8
|
|
10
|
-
#
|
9
|
+
# @return [String] The JWA name of the algorithm.
|
10
|
+
attr_reader :name
|
11
|
+
|
12
|
+
# Creates a new instance; it's probably easier to use one of the subclass constructors.
|
13
|
+
#
|
14
|
+
# @param sha_size [Integer] The size of the SHA algorithm.
|
15
|
+
# @param key [String] The key to use for signing or validation.
|
11
16
|
def initialize(sha_size, key)
|
12
|
-
raise ArgumentError, 'A key is required.' unless key
|
13
17
|
@name = "HS#{sha_size}"
|
14
18
|
@digest = OpenSSL::Digest.new("sha#{sha_size}")
|
15
19
|
@key = key
|
16
20
|
end
|
17
21
|
|
18
22
|
# Signs a payload and returns the signature.
|
23
|
+
#
|
24
|
+
# @param payload [String] The payload of the token to sign.
|
25
|
+
# @return [String] The signature.
|
19
26
|
def sign(payload)
|
20
27
|
OpenSSL::HMAC.digest(@digest, @key, payload)
|
21
28
|
end
|
22
29
|
|
23
|
-
#
|
24
|
-
|
30
|
+
# Validates a payload signature and returns whether the signature matches.
|
31
|
+
#
|
32
|
+
# @param signature [String] The signature to verify.
|
33
|
+
# @param payload [String] The payload of the token.
|
34
|
+
# @return [Boolean] true if the signature is correct; otherwise false.
|
35
|
+
def valid?(signature, payload)
|
25
36
|
Sandal::Util.secure_equals(sign(payload), signature)
|
26
37
|
end
|
27
38
|
|
@@ -29,7 +40,9 @@ module Sandal
|
|
29
40
|
|
30
41
|
# The HMAC-SHA256 signing algorithm.
|
31
42
|
class HS256 < Sandal::Sig::HS
|
32
|
-
# Creates a new instance
|
43
|
+
# Creates a new instance.
|
44
|
+
#
|
45
|
+
# @param key [String] The key to use for signing or validation.
|
33
46
|
def initialize(key)
|
34
47
|
super(256, key)
|
35
48
|
end
|
@@ -37,7 +50,9 @@ module Sandal
|
|
37
50
|
|
38
51
|
# The HMAC-SHA384 signing algorithm.
|
39
52
|
class HS384 < Sandal::Sig::HS
|
40
|
-
# Creates a new instance
|
53
|
+
# Creates a new instance.
|
54
|
+
#
|
55
|
+
# @param key [String] The key to use for signing or validation.
|
41
56
|
def initialize(key)
|
42
57
|
super(384, key)
|
43
58
|
end
|
@@ -45,7 +60,9 @@ module Sandal
|
|
45
60
|
|
46
61
|
# The HMAC-SHA512 signing algorithm.
|
47
62
|
class HS512 < Sandal::Sig::HS
|
48
|
-
# Creates a new instance
|
63
|
+
# Creates a new instance.
|
64
|
+
#
|
65
|
+
# @param key [String] The key to use for signing or validation.
|
49
66
|
def initialize(key)
|
50
67
|
super(512, key)
|
51
68
|
end
|
data/lib/sandal/sig/rs.rb
CHANGED
@@ -5,27 +5,35 @@ module Sandal
|
|
5
5
|
|
6
6
|
# Base implementation of the RSA-SHA family of signature algorithms.
|
7
7
|
class RS
|
8
|
-
include Sandal::Sig
|
9
8
|
|
10
|
-
#
|
11
|
-
|
12
|
-
|
13
|
-
#
|
9
|
+
# @return [String] The JWA name of the algorithm.
|
10
|
+
attr_reader :name
|
11
|
+
|
12
|
+
# Creates a new instance; it's probably easier to use one of the subclass constructors.
|
13
|
+
#
|
14
|
+
# @param sha_size [Integer] The size of the SHA algorithm.
|
15
|
+
# @param key [OpenSSL::PKey::RSA] The key to use for signing (private) or validation (public). This must
|
16
|
+
# be at least 2048 bits to be compliant with the JWA specification.
|
14
17
|
def initialize(sha_size, key)
|
15
|
-
raise ArgumentError, 'A key is required.' unless key
|
16
18
|
@name = "RS#{sha_size}"
|
17
19
|
@digest = OpenSSL::Digest.new("sha#{sha_size}")
|
18
20
|
@key = key
|
19
21
|
end
|
20
22
|
|
21
23
|
# Signs a payload and returns the signature.
|
24
|
+
#
|
25
|
+
# @param payload [String] The payload of the token to sign.
|
26
|
+
# @return [String] The signature.
|
22
27
|
def sign(payload)
|
23
|
-
raise ArgumentError, 'A private key is required to sign the payload.' unless @key.private?
|
24
28
|
@key.sign(@digest, payload)
|
25
29
|
end
|
26
30
|
|
27
|
-
#
|
28
|
-
|
31
|
+
# Validates a payload signature and returns whether the signature matches.
|
32
|
+
#
|
33
|
+
# @param signature [String] The signature to verify.
|
34
|
+
# @param payload [String] The payload of the token.
|
35
|
+
# @return [Boolean] true if the signature is correct; otherwise false.
|
36
|
+
def valid?(signature, payload)
|
29
37
|
@key.verify(@digest, signature, payload)
|
30
38
|
end
|
31
39
|
|
@@ -33,7 +41,10 @@ module Sandal
|
|
33
41
|
|
34
42
|
# The RSA-SHA256 signing algorithm.
|
35
43
|
class RS256 < Sandal::Sig::RS
|
36
|
-
# Creates a new instance
|
44
|
+
# Creates a new instance.
|
45
|
+
#
|
46
|
+
# @param key [OpenSSL::PKey::RSA] The key to use for signing (private) or validation (public). This must
|
47
|
+
# be at least 2048 bits to be compliant with the JWA specification.
|
37
48
|
def initialize(key)
|
38
49
|
super(256, key)
|
39
50
|
end
|
@@ -41,7 +52,10 @@ module Sandal
|
|
41
52
|
|
42
53
|
# The RSA-SHA384 signing algorithm.
|
43
54
|
class RS384 < Sandal::Sig::RS
|
44
|
-
# Creates a new instance
|
55
|
+
# Creates a new instance.
|
56
|
+
#
|
57
|
+
# @param key [OpenSSL::PKey::RSA] The key to use for signing (private) or validation (public). This must
|
58
|
+
# be at least 2048 bits to be compliant with the JWA specification.
|
45
59
|
def initialize(key)
|
46
60
|
super(384, key)
|
47
61
|
end
|
@@ -49,7 +63,10 @@ module Sandal
|
|
49
63
|
|
50
64
|
# The RSA-SHA512 signing algorithm.
|
51
65
|
class RS512 < Sandal::Sig::RS
|
52
|
-
# Creates a new instance
|
66
|
+
# Creates a new instance.
|
67
|
+
#
|
68
|
+
# @param key [OpenSSL::PKey::RSA] The key to use for signing (private) or validation (public). This must
|
69
|
+
# be at least 2048 bits to be compliant with the JWA specification.
|
53
70
|
def initialize(key)
|
54
71
|
super(512, key)
|
55
72
|
end
|
data/lib/sandal/sig.rb
CHANGED
@@ -1,34 +1,16 @@
|
|
1
1
|
require 'singleton'
|
2
2
|
|
3
3
|
module Sandal
|
4
|
-
#
|
4
|
+
# Contains signature (JWS) functionality.
|
5
5
|
module Sig
|
6
6
|
|
7
|
-
# @return [String] The JWA name of the algorithm.
|
8
|
-
attr_reader :name
|
9
|
-
|
10
|
-
# Signs a payload and returns the signature.
|
11
|
-
#
|
12
|
-
# @param payload [String] The payload of the token to sign.
|
13
|
-
# @return [String] The signature.
|
14
|
-
def sign(payload)
|
15
|
-
raise NotImplementedError, "#{@name}.sign is not implemented."
|
16
|
-
end
|
17
|
-
|
18
|
-
# Verifies a payload signature and returns whether the signature matches.
|
19
|
-
#
|
20
|
-
# @param signature [String] The signature to verify.
|
21
|
-
# @param payload [String] The payload of the token.
|
22
|
-
# @return [Boolean] true if the signature is correct; otherwise false.
|
23
|
-
def verify(signature, payload)
|
24
|
-
raise NotImplementedError, "#{@name}.verify is not implemented."
|
25
|
-
end
|
26
|
-
|
27
7
|
# The 'none' JWA signature method.
|
28
8
|
class None
|
29
|
-
include Sandal::Sig
|
30
9
|
include Singleton
|
31
10
|
|
11
|
+
# @return [String] The JWA name of the algorithm.
|
12
|
+
attr_reader :name
|
13
|
+
|
32
14
|
# Creates a new instance.
|
33
15
|
def initialize
|
34
16
|
@name = 'none'
|
@@ -42,13 +24,13 @@ module Sandal
|
|
42
24
|
''
|
43
25
|
end
|
44
26
|
|
45
|
-
#
|
27
|
+
# Validates that a signature is nil or empty.
|
46
28
|
#
|
47
29
|
# @param signature [String] The signature to verify.
|
48
30
|
# @param payload [String] This parameter is ignored.
|
49
|
-
# @return [Boolean]
|
50
|
-
def
|
51
|
-
signature.nil? || signature.
|
31
|
+
# @return [Boolean] true if the signature is nil or empty; otherwise false.
|
32
|
+
def valid?(signature, payload)
|
33
|
+
signature.nil? || signature.empty?
|
52
34
|
end
|
53
35
|
|
54
36
|
end
|
data/lib/sandal/util.rb
CHANGED
@@ -21,12 +21,20 @@ module Sandal
|
|
21
21
|
end
|
22
22
|
|
23
23
|
# Base64 encodes a string, in compliance with the JWT specification.
|
24
|
+
#
|
25
|
+
# @param s [String] The string to encode.
|
26
|
+
# @return [String] The encoded base64 string.
|
24
27
|
def self.base64_encode(s)
|
25
28
|
Base64.urlsafe_encode64(s).gsub(%r{=+$}, '')
|
26
29
|
end
|
27
30
|
|
28
31
|
# Base64 decodes a string, in compliance with the JWT specification.
|
32
|
+
#
|
33
|
+
# @param s [String] The base64 string to decode.
|
34
|
+
# @return [String] The decoded string.
|
35
|
+
# @raise [Sandal::TokenError] The base64 string contains padding.
|
29
36
|
def self.base64_decode(s)
|
37
|
+
raise Sandal::TokenError, 'Base64 strings cannot contain padding.' if s.end_with?('=')
|
30
38
|
padding_length = (4 - (s.length % 4)) % 4
|
31
39
|
padding = '=' * padding_length
|
32
40
|
Base64.urlsafe_decode64(s + padding)
|
data/lib/sandal/version.rb
CHANGED
data/lib/sandal.rb
CHANGED
@@ -1,12 +1,11 @@
|
|
1
1
|
$:.unshift('.')
|
2
2
|
|
3
3
|
require 'base64'
|
4
|
-
require '
|
4
|
+
require 'multi_json'
|
5
5
|
require 'openssl'
|
6
|
-
|
6
|
+
require 'sandal/claims'
|
7
|
+
require 'sandal/util'
|
7
8
|
require 'sandal/version'
|
8
|
-
require 'sandal/sig'
|
9
|
-
require 'sandal/enc'
|
10
9
|
|
11
10
|
# A library for creating and reading JSON Web Tokens (JWT).
|
12
11
|
module Sandal
|
@@ -14,6 +13,9 @@ module Sandal
|
|
14
13
|
# The error that is raised when a token is invalid.
|
15
14
|
class TokenError < StandardError; end
|
16
15
|
|
16
|
+
# The error that is raised when a claim within a token is invalid.
|
17
|
+
class ClaimError < TokenError; end
|
18
|
+
|
17
19
|
# The default options for token handling.
|
18
20
|
#
|
19
21
|
# max_clock_skew:: The maximum clock skew, in seconds, when validating times.
|
@@ -43,23 +45,20 @@ module Sandal
|
|
43
45
|
|
44
46
|
# Creates a signed JSON Web Token.
|
45
47
|
#
|
46
|
-
# @param payload [String/Hash] The payload of the token.
|
47
|
-
# @param signer [
|
48
|
+
# @param payload [String/Hash] The payload of the token. Hashes will be encoded as JSON.
|
49
|
+
# @param signer [#name,#sign] The token signer, which may be nil for an unsigned token.
|
48
50
|
# @param header_fields [Hash] Header fields for the token (note: do not include 'alg').
|
49
51
|
# @return [String] A signed JSON Web Token.
|
50
52
|
def self.encode_token(payload, signer, header_fields = nil)
|
51
|
-
if header_fields && header_fields['enc']
|
52
|
-
raise ArgumentError, 'The header cannot contain an "enc" parameter.'
|
53
|
-
end
|
54
53
|
signer ||= Sandal::Sig::None.instance
|
55
54
|
|
56
55
|
header = {}
|
57
56
|
header['alg'] = signer.name if signer.name != Sandal::Sig::None.instance.name
|
58
57
|
header = header_fields.merge(header) if header_fields
|
59
58
|
|
60
|
-
payload =
|
59
|
+
payload = MultiJson.dump(payload) unless payload.is_a?(String)
|
61
60
|
|
62
|
-
encoded_header = Sandal::Util.base64_encode(
|
61
|
+
encoded_header = Sandal::Util.base64_encode(MultiJson.dump(header))
|
63
62
|
encoded_payload = Sandal::Util.base64_encode(payload)
|
64
63
|
secured_input = [encoded_header, encoded_payload].join('.')
|
65
64
|
|
@@ -86,47 +85,29 @@ module Sandal
|
|
86
85
|
# Decodes and validates a JSON Web Token.
|
87
86
|
#
|
88
87
|
# The block is called with the token header as the first parameter, and should return the appropriate
|
89
|
-
# {Sandal::Sig} to
|
88
|
+
# {Sandal::Sig} to validate the signature. It can optionally have a second options parameter which can
|
90
89
|
# be used to override the {DEFAULT_OPTIONS} on a per-token basis.
|
91
90
|
#
|
92
91
|
# @param token [String] The encoded JSON Web Token.
|
93
92
|
# @yieldparam header [Hash] The JWT header values.
|
94
93
|
# @yieldparam options [Hash] (Optional) A hash that can be used to override the default options.
|
95
|
-
# @yieldreturn [
|
94
|
+
# @yieldreturn [#valid?] The signature validator.
|
96
95
|
# @return [Hash/String] The payload of the token as a Hash if it was JSON, otherwise as a String.
|
97
|
-
# @raise [
|
98
|
-
|
99
|
-
def self.decode_token(token, &block)
|
100
|
-
raise ArgumentError, 'A token is required.' unless token && token.length > 0
|
96
|
+
# @raise [Sandal::TokenError] The token format is invalid, or validation of the token failed.
|
97
|
+
def self.decode_token(token)
|
101
98
|
parts = token.split('.')
|
102
|
-
|
103
|
-
begin
|
104
|
-
header = JSON.parse(Sandal::Util.base64_decode(parts[0]))
|
105
|
-
payload = Sandal::Util.base64_decode(parts[1])
|
106
|
-
signature = if parts.length > 2 then Sandal::Util.base64_decode(parts[2]) else '' end
|
107
|
-
rescue
|
108
|
-
raise TokenError, 'Invalid token encoding.'
|
109
|
-
end
|
99
|
+
header, payload, signature = decode_jws_parts(parts)
|
110
100
|
|
111
101
|
options = DEFAULT_OPTIONS.clone
|
112
|
-
if
|
113
|
-
|
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
|
102
|
+
validator = yield header, options if block_given?
|
103
|
+
validator ||= Sandal::Sig::None.instance
|
120
104
|
|
121
105
|
if options[:validate_signature]
|
122
106
|
secured_input = parts.take(2).join('.')
|
123
|
-
raise TokenError, 'Invalid signature.' unless
|
107
|
+
raise TokenError, 'Invalid signature.' unless validator.valid?(signature, secured_input)
|
124
108
|
end
|
125
109
|
|
126
|
-
|
127
|
-
validate_claims(claims, options) if claims
|
128
|
-
|
129
|
-
claims || payload
|
110
|
+
parse_and_validate(payload, header['cty'], options)
|
130
111
|
end
|
131
112
|
|
132
113
|
# Decrypts an encrypted JSON Web Token.
|
@@ -136,7 +117,7 @@ module Sandal
|
|
136
117
|
parts = encrypted_token.split('.')
|
137
118
|
raise ArgumentError, 'Invalid token format.' unless parts.length == 5
|
138
119
|
begin
|
139
|
-
header =
|
120
|
+
header = MultiJson.load(Sandal::Util.base64_decode(parts[0]))
|
140
121
|
encrypted_key = Sandal::Util.base64_decode(parts[1])
|
141
122
|
iv = Sandal::Util.base64_decode(parts[2])
|
142
123
|
ciphertext = Sandal::Util.base64_decode(parts[3])
|
@@ -152,54 +133,30 @@ module Sandal
|
|
152
133
|
|
153
134
|
private
|
154
135
|
|
155
|
-
#
|
156
|
-
def self.
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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
|
174
|
-
|
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])
|
136
|
+
# Decodes the parts of a JWS token.
|
137
|
+
def self.decode_jws_parts(parts)
|
138
|
+
raise TokenError, 'Invalid token format.' unless [2, 3].include?(parts.length)
|
139
|
+
begin
|
140
|
+
header = MultiJson.load(Sandal::Util.base64_decode(parts[0]))
|
141
|
+
payload = Sandal::Util.base64_decode(parts[1])
|
142
|
+
signature = if parts.length > 2 then Sandal::Util.base64_decode(parts[2]) else '' end
|
143
|
+
rescue
|
144
|
+
raise TokenError, 'Invalid token encoding.'
|
184
145
|
end
|
146
|
+
return header, payload, signature
|
185
147
|
end
|
186
148
|
|
187
|
-
#
|
188
|
-
def self.
|
189
|
-
|
190
|
-
if
|
191
|
-
|
149
|
+
# Parses the content of a token and validates the claims if is a JSON claim set.
|
150
|
+
def self.parse_and_validate(payload, content_type, options)
|
151
|
+
claims = MultiJson.load(payload) rescue nil unless content_type == 'JWT'
|
152
|
+
if claims
|
153
|
+
claims.extend(Sandal::Claims).validate_claims(options)
|
154
|
+
else
|
155
|
+
payload
|
192
156
|
end
|
193
157
|
end
|
194
158
|
|
195
|
-
|
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
|
159
|
+
end
|
204
160
|
|
205
|
-
|
161
|
+
require 'sandal/enc'
|
162
|
+
require 'sandal/sig'
|
data/sandal.gemspec
CHANGED
@@ -17,16 +17,16 @@ Gem::Specification.new do |s|
|
|
17
17
|
s.require_paths = ['lib']
|
18
18
|
s.extra_rdoc_files = ['README.md', 'LICENSE.md']
|
19
19
|
|
20
|
-
s.add_runtime_dependency '
|
20
|
+
s.add_runtime_dependency 'multi_json', '~> 1.7'
|
21
21
|
s.add_runtime_dependency 'jruby-openssl', '~> 0.7', '>= 0.7.3' if RUBY_PLATFORM == 'java'
|
22
22
|
|
23
|
-
s.add_development_dependency 'bundler', '
|
24
|
-
s.add_development_dependency 'rake', '
|
25
|
-
s.add_development_dependency 'rspec', '
|
26
|
-
s.add_development_dependency 'coveralls', '
|
27
|
-
s.add_development_dependency 'yard', '
|
28
|
-
s.add_development_dependency 'redcarpet', '
|
29
|
-
s.add_development_dependency 'kramdown', '
|
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
30
|
|
31
31
|
s.requirements << 'openssl 1.0.1c for EC signature methods'
|
32
32
|
end
|
data/spec/sandal/sig/es_spec.rb
CHANGED
@@ -55,8 +55,8 @@ describe Sandal::Sig::ES256 do
|
|
55
55
|
signature = signer.sign(data)
|
56
56
|
public_key = OpenSSL::PKey::EC.new(group)
|
57
57
|
public_key.public_key = private_key.public_key
|
58
|
-
|
59
|
-
|
58
|
+
validator = Sandal::Sig::ES256.new(public_key)
|
59
|
+
validator.valid?(signature, data).should == true
|
60
60
|
end
|
61
61
|
|
62
62
|
it 'can verify the signature in JWS section A3.1' do
|
@@ -69,8 +69,8 @@ describe Sandal::Sig::ES256 do
|
|
69
69
|
group = OpenSSL::PKey::EC::Group.new('prime256v1')
|
70
70
|
public_key = OpenSSL::PKey::EC.new(group)
|
71
71
|
public_key.public_key = make_point(group, x, y)
|
72
|
-
|
73
|
-
|
72
|
+
validator = Sandal::Sig::ES256.new(public_key)
|
73
|
+
validator.valid?(signature, data).should == true
|
74
74
|
end
|
75
75
|
|
76
76
|
it 'fails to verify the signature in JWS section A3.1 when the data is changed' do
|
@@ -83,8 +83,14 @@ describe Sandal::Sig::ES256 do
|
|
83
83
|
group = OpenSSL::PKey::EC::Group.new('prime256v1')
|
84
84
|
public_key = OpenSSL::PKey::EC.new(group)
|
85
85
|
public_key.public_key = make_point(group, x, y)
|
86
|
-
|
87
|
-
|
86
|
+
validator = Sandal::Sig::ES256.new(public_key)
|
87
|
+
validator.valid?(signature, data).should == false
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'raises an argument error if the key has the wrong curve' do
|
91
|
+
group = OpenSSL::PKey::EC::Group.new('secp384r1')
|
92
|
+
private_key = OpenSSL::PKey::EC.new(group).generate_key
|
93
|
+
expect { Sandal::Sig::ES256.new(private_key) }.to raise_error ArgumentError
|
88
94
|
end
|
89
95
|
|
90
96
|
end
|
@@ -99,8 +105,14 @@ describe Sandal::Sig::ES384 do
|
|
99
105
|
signature = signer.sign(data)
|
100
106
|
public_key = OpenSSL::PKey::EC.new(group)
|
101
107
|
public_key.public_key = private_key.public_key
|
102
|
-
|
103
|
-
|
108
|
+
validator = Sandal::Sig::ES384.new(public_key)
|
109
|
+
validator.valid?(signature, data).should == true
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'raises an argument error if the key has the wrong curve' do
|
113
|
+
group = OpenSSL::PKey::EC::Group.new('secp521r1')
|
114
|
+
private_key = OpenSSL::PKey::EC.new(group).generate_key
|
115
|
+
expect { Sandal::Sig::ES384.new(private_key) }.to raise_error ArgumentError
|
104
116
|
end
|
105
117
|
|
106
118
|
end
|
@@ -115,8 +127,8 @@ describe Sandal::Sig::ES512 do
|
|
115
127
|
signature = signer.sign(data)
|
116
128
|
public_key = OpenSSL::PKey::EC.new(group)
|
117
129
|
public_key.public_key = private_key.public_key
|
118
|
-
|
119
|
-
|
130
|
+
validator = Sandal::Sig::ES512.new(public_key)
|
131
|
+
validator.valid?(signature, data).should == true
|
120
132
|
end
|
121
133
|
|
122
134
|
it 'can verify the signature in JWS section A4.1' do
|
@@ -129,8 +141,8 @@ describe Sandal::Sig::ES512 do
|
|
129
141
|
group = OpenSSL::PKey::EC::Group.new('secp521r1')
|
130
142
|
public_key = OpenSSL::PKey::EC.new(group)
|
131
143
|
public_key.public_key = make_point(group, x, y)
|
132
|
-
|
133
|
-
|
144
|
+
validator = Sandal::Sig::ES512.new(public_key)
|
145
|
+
validator.valid?(signature, data).should == true
|
134
146
|
end
|
135
147
|
|
136
148
|
it 'fails to verify the signature in JWS section A4.1 when the data is changed' do
|
@@ -143,8 +155,14 @@ describe Sandal::Sig::ES512 do
|
|
143
155
|
group = OpenSSL::PKey::EC::Group.new('secp521r1')
|
144
156
|
public_key = OpenSSL::PKey::EC.new(group)
|
145
157
|
public_key.public_key = make_point(group, x, y)
|
146
|
-
|
147
|
-
|
158
|
+
validator = Sandal::Sig::ES512.new(public_key)
|
159
|
+
validator.valid?(signature, data).should == false
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'raises an argument error if the key has the wrong curve' do
|
163
|
+
group = OpenSSL::PKey::EC::Group.new('prime256v1')
|
164
|
+
private_key = OpenSSL::PKey::EC.new(group).generate_key
|
165
|
+
expect { Sandal::Sig::ES512.new(private_key) }.to raise_error ArgumentError
|
148
166
|
end
|
149
167
|
|
150
168
|
end
|
data/spec/sandal/sig/hs_spec.rb
CHANGED
@@ -7,7 +7,7 @@ describe Sandal::Sig::HS256 do
|
|
7
7
|
key = 'A secret key'
|
8
8
|
signer = Sandal::Sig::HS256.new(key)
|
9
9
|
signature = signer.sign(data)
|
10
|
-
signer.
|
10
|
+
signer.valid?(signature, data).should == true
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
@@ -17,7 +17,7 @@ describe Sandal::Sig::HS384 do
|
|
17
17
|
key = 'Another secret key'
|
18
18
|
signer = Sandal::Sig::HS384.new(key)
|
19
19
|
signature = signer.sign(data)
|
20
|
-
signer.
|
20
|
+
signer.valid?(signature, data).should == true
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
@@ -27,6 +27,6 @@ describe Sandal::Sig::HS512 do
|
|
27
27
|
key = 'Yet another secret key'
|
28
28
|
signer = Sandal::Sig::HS512.new(key)
|
29
29
|
signature = signer.sign(data)
|
30
|
-
signer.
|
30
|
+
signer.valid?(signature, data).should == true
|
31
31
|
end
|
32
32
|
end
|
data/spec/sandal/sig/rs_spec.rb
CHANGED
@@ -7,8 +7,8 @@ describe Sandal::Sig::RS256 do
|
|
7
7
|
private_key = OpenSSL::PKey::RSA.generate(2048)
|
8
8
|
signer = Sandal::Sig::RS256.new(private_key)
|
9
9
|
signature = signer.sign(data)
|
10
|
-
|
11
|
-
|
10
|
+
validator = Sandal::Sig::RS256.new(private_key.public_key)
|
11
|
+
validator.valid?(signature, data).should == true
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
@@ -18,8 +18,8 @@ describe Sandal::Sig::RS384 do
|
|
18
18
|
private_key = OpenSSL::PKey::RSA.generate(2048)
|
19
19
|
signer = Sandal::Sig::RS384.new(private_key)
|
20
20
|
signature = signer.sign(data)
|
21
|
-
|
22
|
-
|
21
|
+
validator = Sandal::Sig::RS384.new(private_key.public_key)
|
22
|
+
validator.valid?(signature, data).should == true
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
@@ -29,7 +29,7 @@ describe Sandal::Sig::RS512 do
|
|
29
29
|
private_key = OpenSSL::PKey::RSA.generate(2048)
|
30
30
|
signer = Sandal::Sig::RS512.new(private_key)
|
31
31
|
signature = signer.sign(data)
|
32
|
-
|
33
|
-
|
32
|
+
validator = Sandal::Sig::RS512.new(private_key.public_key)
|
33
|
+
validator.valid?(signature, data).should == true
|
34
34
|
end
|
35
35
|
end
|
data/spec/sandal/util_spec.rb
CHANGED
@@ -11,6 +11,10 @@ describe Sandal::Util do
|
|
11
11
|
val.should == src
|
12
12
|
end
|
13
13
|
|
14
|
+
it 'raises a token error if base64 strings contain padding' do
|
15
|
+
expect { Sandal::Util.base64_decode('eyJpc3MiOiJq=') }.to raise_error Sandal::TokenError
|
16
|
+
end
|
17
|
+
|
14
18
|
it 'compares nil strings as equal' do
|
15
19
|
Sandal::Util.secure_equals(nil, nil).should == true
|
16
20
|
end
|
data/spec/sandal_spec.rb
CHANGED
@@ -27,22 +27,22 @@ describe Sandal do
|
|
27
27
|
|
28
28
|
it 'decodes non-JSON payloads to a String' do
|
29
29
|
token = Sandal.encode_token('not valid json', nil)
|
30
|
-
Sandal.decode_token(token).class.should
|
30
|
+
Sandal.decode_token(token).class.should.kind_of? String
|
31
31
|
end
|
32
32
|
|
33
33
|
it 'decodes JSON payloads to a Hash' do
|
34
34
|
token = Sandal.encode_token({ 'valid' => 'json' }, nil)
|
35
|
-
Sandal.decode_token(token).class.should
|
35
|
+
Sandal.decode_token(token).class.should.kind_of? Hash
|
36
36
|
end
|
37
37
|
|
38
|
-
it 'raises a
|
38
|
+
it 'raises a claim error when the expiry date is far in the past' do
|
39
39
|
token = Sandal.encode_token({ 'exp' => (Time.now - 600).to_i }, nil)
|
40
|
-
expect { Sandal.decode_token(token) }.to raise_error Sandal::
|
40
|
+
expect { Sandal.decode_token(token) }.to raise_error Sandal::ClaimError
|
41
41
|
end
|
42
42
|
|
43
|
-
it 'raises a
|
43
|
+
it 'raises a claim error when the expiry date is invalid' do
|
44
44
|
token = Sandal.encode_token({ 'exp' => 'invalid value' }, nil)
|
45
|
-
expect { Sandal.decode_token(token) }.to raise_error Sandal::
|
45
|
+
expect { Sandal.decode_token(token) }.to raise_error Sandal::ClaimError
|
46
46
|
end
|
47
47
|
|
48
48
|
it 'does not raise an error when the expiry date is far in the past but validation is disabled' do
|
@@ -60,14 +60,14 @@ describe Sandal do
|
|
60
60
|
Sandal.decode_token(token)
|
61
61
|
end
|
62
62
|
|
63
|
-
it 'raises a
|
63
|
+
it 'raises a claim error when the not-before date is far in the future' do
|
64
64
|
token = Sandal.encode_token({ 'nbf' => (Time.now + 600).to_i }, nil)
|
65
|
-
expect { Sandal.decode_token(token) }.to raise_error Sandal::
|
65
|
+
expect { Sandal.decode_token(token) }.to raise_error Sandal::ClaimError
|
66
66
|
end
|
67
67
|
|
68
|
-
it 'raises a
|
68
|
+
it 'raises a claim error when the not-before date is invalid' do
|
69
69
|
token = Sandal.encode_token({ 'nbf' => 'invalid value' }, nil)
|
70
|
-
expect { Sandal.decode_token(token) }.to raise_error Sandal::
|
70
|
+
expect { Sandal.decode_token(token) }.to raise_error Sandal::ClaimError
|
71
71
|
end
|
72
72
|
|
73
73
|
it 'does not raise an error when the not-before date is far in the future but validation is disabled' do
|
@@ -85,12 +85,12 @@ describe Sandal do
|
|
85
85
|
Sandal.decode_token(token)
|
86
86
|
end
|
87
87
|
|
88
|
-
it 'raises a
|
88
|
+
it 'raises a claim error when the issuer is not valid' do
|
89
89
|
token = Sandal.encode_token({ 'iss' => 'example.org' }, nil)
|
90
90
|
expect { Sandal.decode_token(token) do |header, options|
|
91
91
|
options[:valid_iss] = ['example.net']
|
92
92
|
nil
|
93
|
-
end }.to raise_error Sandal::
|
93
|
+
end }.to raise_error Sandal::ClaimError
|
94
94
|
end
|
95
95
|
|
96
96
|
it 'does not raise an error when the issuer is valid' do
|
@@ -101,20 +101,20 @@ describe Sandal do
|
|
101
101
|
end
|
102
102
|
end
|
103
103
|
|
104
|
-
it 'raises a
|
104
|
+
it 'raises a claim error when the audience string is not valid' do
|
105
105
|
token = Sandal.encode_token({ 'aud' => 'example.com' }, nil)
|
106
106
|
expect { Sandal.decode_token(token) do |header, options|
|
107
107
|
options[:valid_aud] = ['example.net']
|
108
108
|
nil
|
109
|
-
end }.to raise_error Sandal::
|
109
|
+
end }.to raise_error Sandal::ClaimError
|
110
110
|
end
|
111
111
|
|
112
|
-
it 'raises a
|
112
|
+
it 'raises a claim error when the audience array is not valid' do
|
113
113
|
token = Sandal.encode_token({ 'aud' => ['example.org', 'example.com'] }, nil)
|
114
114
|
expect { Sandal.decode_token(token) do |header, options|
|
115
115
|
options[:valid_aud] = ['example.net']
|
116
116
|
nil
|
117
|
-
end }.to raise_error Sandal::
|
117
|
+
end }.to raise_error Sandal::ClaimError
|
118
118
|
end
|
119
119
|
|
120
120
|
it 'does not raise an error when the audience string is valid' do
|
metadata
CHANGED
@@ -1,20 +1,18 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sandal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
5
|
-
prerelease:
|
4
|
+
version: 0.1.1
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Greg Beech
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date: 2013-
|
11
|
+
date: 2013-04-01 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
14
|
+
name: multi_json
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
17
|
- - ~>
|
20
18
|
- !ruby/object:Gem::Version
|
@@ -22,7 +20,6 @@ dependencies:
|
|
22
20
|
type: :runtime
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
24
|
- - ~>
|
28
25
|
- !ruby/object:Gem::Version
|
@@ -30,97 +27,85 @@ dependencies:
|
|
30
27
|
- !ruby/object:Gem::Dependency
|
31
28
|
name: bundler
|
32
29
|
requirement: !ruby/object:Gem::Requirement
|
33
|
-
none: false
|
34
30
|
requirements:
|
35
|
-
- -
|
31
|
+
- - ! '>='
|
36
32
|
- !ruby/object:Gem::Version
|
37
33
|
version: '1.3'
|
38
34
|
type: :development
|
39
35
|
prerelease: false
|
40
36
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
37
|
requirements:
|
43
|
-
- -
|
38
|
+
- - ! '>='
|
44
39
|
- !ruby/object:Gem::Version
|
45
40
|
version: '1.3'
|
46
41
|
- !ruby/object:Gem::Dependency
|
47
42
|
name: rake
|
48
43
|
requirement: !ruby/object:Gem::Requirement
|
49
|
-
none: false
|
50
44
|
requirements:
|
51
|
-
- -
|
45
|
+
- - ! '>='
|
52
46
|
- !ruby/object:Gem::Version
|
53
47
|
version: '10.0'
|
54
48
|
type: :development
|
55
49
|
prerelease: false
|
56
50
|
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
none: false
|
58
51
|
requirements:
|
59
|
-
- -
|
52
|
+
- - ! '>='
|
60
53
|
- !ruby/object:Gem::Version
|
61
54
|
version: '10.0'
|
62
55
|
- !ruby/object:Gem::Dependency
|
63
56
|
name: rspec
|
64
57
|
requirement: !ruby/object:Gem::Requirement
|
65
|
-
none: false
|
66
58
|
requirements:
|
67
|
-
- -
|
59
|
+
- - ! '>='
|
68
60
|
- !ruby/object:Gem::Version
|
69
61
|
version: '2.13'
|
70
62
|
type: :development
|
71
63
|
prerelease: false
|
72
64
|
version_requirements: !ruby/object:Gem::Requirement
|
73
|
-
none: false
|
74
65
|
requirements:
|
75
|
-
- -
|
66
|
+
- - ! '>='
|
76
67
|
- !ruby/object:Gem::Version
|
77
68
|
version: '2.13'
|
78
69
|
- !ruby/object:Gem::Dependency
|
79
70
|
name: coveralls
|
80
71
|
requirement: !ruby/object:Gem::Requirement
|
81
|
-
none: false
|
82
72
|
requirements:
|
83
|
-
- -
|
73
|
+
- - ! '>='
|
84
74
|
- !ruby/object:Gem::Version
|
85
75
|
version: '0.6'
|
86
76
|
type: :development
|
87
77
|
prerelease: false
|
88
78
|
version_requirements: !ruby/object:Gem::Requirement
|
89
|
-
none: false
|
90
79
|
requirements:
|
91
|
-
- -
|
80
|
+
- - ! '>='
|
92
81
|
- !ruby/object:Gem::Version
|
93
82
|
version: '0.6'
|
94
83
|
- !ruby/object:Gem::Dependency
|
95
84
|
name: yard
|
96
85
|
requirement: !ruby/object:Gem::Requirement
|
97
|
-
none: false
|
98
86
|
requirements:
|
99
|
-
- -
|
87
|
+
- - ! '>='
|
100
88
|
- !ruby/object:Gem::Version
|
101
89
|
version: '0.8'
|
102
90
|
type: :development
|
103
91
|
prerelease: false
|
104
92
|
version_requirements: !ruby/object:Gem::Requirement
|
105
|
-
none: false
|
106
93
|
requirements:
|
107
|
-
- -
|
94
|
+
- - ! '>='
|
108
95
|
- !ruby/object:Gem::Version
|
109
96
|
version: '0.8'
|
110
97
|
- !ruby/object:Gem::Dependency
|
111
98
|
name: redcarpet
|
112
99
|
requirement: !ruby/object:Gem::Requirement
|
113
|
-
none: false
|
114
100
|
requirements:
|
115
|
-
- -
|
101
|
+
- - ! '>='
|
116
102
|
- !ruby/object:Gem::Version
|
117
103
|
version: '2.2'
|
118
104
|
type: :development
|
119
105
|
prerelease: false
|
120
106
|
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
none: false
|
122
107
|
requirements:
|
123
|
-
- -
|
108
|
+
- - ! '>='
|
124
109
|
- !ruby/object:Gem::Version
|
125
110
|
version: '2.2'
|
126
111
|
description: A ruby library for creating and reading JSON Web Tokens (JWT), supporting
|
@@ -143,6 +128,7 @@ files:
|
|
143
128
|
- README.md
|
144
129
|
- Rakefile
|
145
130
|
- lib/sandal.rb
|
131
|
+
- lib/sandal/claims.rb
|
146
132
|
- lib/sandal/enc.rb
|
147
133
|
- lib/sandal/enc/aescbc.rb
|
148
134
|
- lib/sandal/enc/aesgcm.rb
|
@@ -162,34 +148,27 @@ files:
|
|
162
148
|
homepage: http://rubygems.org/gems/sandal
|
163
149
|
licenses:
|
164
150
|
- MIT
|
151
|
+
metadata: {}
|
165
152
|
post_install_message:
|
166
153
|
rdoc_options: []
|
167
154
|
require_paths:
|
168
155
|
- lib
|
169
156
|
required_ruby_version: !ruby/object:Gem::Requirement
|
170
|
-
none: false
|
171
157
|
requirements:
|
172
158
|
- - ! '>='
|
173
159
|
- !ruby/object:Gem::Version
|
174
160
|
version: '0'
|
175
|
-
segments:
|
176
|
-
- 0
|
177
|
-
hash: -3672992457221215645
|
178
161
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
179
|
-
none: false
|
180
162
|
requirements:
|
181
163
|
- - ! '>='
|
182
164
|
- !ruby/object:Gem::Version
|
183
165
|
version: '0'
|
184
|
-
segments:
|
185
|
-
- 0
|
186
|
-
hash: -3672992457221215645
|
187
166
|
requirements:
|
188
167
|
- openssl 1.0.1c for EC signature methods
|
189
168
|
rubyforge_project:
|
190
|
-
rubygems_version:
|
169
|
+
rubygems_version: 2.0.3
|
191
170
|
signing_key:
|
192
|
-
specification_version:
|
171
|
+
specification_version: 4
|
193
172
|
summary: A JSON Web Token (JWT) library.
|
194
173
|
test_files:
|
195
174
|
- spec/helper.rb
|