sandal 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![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)
|
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) [![Code Climate](https://codeclimate.com/github/gregbeech/sandal.png)](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
|