jwt-rb 0.0.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 +7 -0
- data/.gitignore +22 -0
- data/.travis.yml +4 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +110 -0
- data/Rakefile +2 -0
- data/jwtrb.gemspec +22 -0
- data/lib/jwt/decoder.rb +97 -0
- data/lib/jwt/encoder.rb +69 -0
- data/lib/jwt/exceptions.rb +10 -0
- data/lib/jwt/payload.rb +8 -0
- data/lib/jwt/signature.rb +35 -0
- data/lib/jwt/verificator.rb +57 -0
- data/lib/jwt/version.rb +3 -0
- data/lib/jwt.rb +24 -0
- data/spec/decoder_spec.rb +153 -0
- data/spec/encoder_spec.rb +96 -0
- data/spec/spec_helper.rb +5 -0
- metadata +121 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 02d0845090b2815ed11a6036bad8cc226b847827
|
4
|
+
data.tar.gz: 72cf2075c4c70df5ddd29b84df559257e49556f0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 71305e4d66010d9facdfff2ed64723958f343c2a27d9d6dbb1bfc489b5a2a993cb3f4850698077d49db8acfd82d9fa0307cda87d8ce9cb787e6a87f577ffcfc2
|
7
|
+
data.tar.gz: a574f05e8dc92466c52d90ac48695ca1ea34fb339a218008cc33a5457ec7b83fa1ca29a1a906c06522952434956c9301d54207566b57002c185c8c7d7e6a33e7
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Luismi Ramirez
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# Jwt-rb
|
2
|
+
[](https://codeclimate.com/github/chupipandi/jwt-rb) [](https://travis-ci.org/chupipandi/jwt-rb)
|
3
|
+
|
4
|
+
## Installation (Coming soon to ruby gems)
|
5
|
+
|
6
|
+
Add this line to your application's Gemfile
|
7
|
+
|
8
|
+
gem 'jwt-rb'
|
9
|
+
|
10
|
+
And then execute:
|
11
|
+
|
12
|
+
$ bundle
|
13
|
+
|
14
|
+
Or install it yourself as:
|
15
|
+
|
16
|
+
$ gem install jwt-rb
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
`jwt-rb` has two methods, one for encoding, one for decoding:
|
21
|
+
|
22
|
+
###JWT.encode(payload, key, options)
|
23
|
+
|
24
|
+
`payload` is a hash which contains the data we want to embed into the token.
|
25
|
+
|
26
|
+
`key` could be a secret string to use in `HMAC` algorithms or a private key for `RSA` algorithms.
|
27
|
+
|
28
|
+
`options`:
|
29
|
+
|
30
|
+
* `algorithm`: Algorithm to use, default is `HS256`.
|
31
|
+
* `claims` (They are optional):
|
32
|
+
* `exp`: Time until the token expires, in seconds.
|
33
|
+
* `aud`: Intended audience of the token.
|
34
|
+
* `iss`: Identifies the issuer of the token.
|
35
|
+
|
36
|
+
####Examples:
|
37
|
+
|
38
|
+
Basic token:
|
39
|
+
```ruby
|
40
|
+
token = JWT.encode({hello: 'world'}, 'thisismysecret')
|
41
|
+
```
|
42
|
+
|
43
|
+
Token with expiration (24 hours):
|
44
|
+
```ruby
|
45
|
+
token = JWT.encode({hello: 'world'}, 'thisismysecret', claim: { exp: 86400 })
|
46
|
+
```
|
47
|
+
|
48
|
+
Token with RSA key:
|
49
|
+
```ruby
|
50
|
+
private_key = OpenSSL::PKey::RSA.generate(512)
|
51
|
+
token = JWT.encode({hello: 'world'}, key, algorithm: 'RS256')
|
52
|
+
```
|
53
|
+
|
54
|
+
###JWT.decode(token, key, options)
|
55
|
+
|
56
|
+
`token` is the JWT string we encoded before.
|
57
|
+
|
58
|
+
`key` could be a secret string to use in `HMAC` algorithms or a private key for `RSA` algorithms.
|
59
|
+
|
60
|
+
`options`:
|
61
|
+
|
62
|
+
* `algorithm` : Algorithm to be used to verify the token.
|
63
|
+
* `claims`:
|
64
|
+
* `aud`: Audience of the token to verify if it matches with the token.
|
65
|
+
* `iss`: Issuer of the token to verify if it matches with the token.
|
66
|
+
|
67
|
+
####Examples:
|
68
|
+
|
69
|
+
Basic decoding:
|
70
|
+
```ruby
|
71
|
+
jwt = 'string containing real jwt token'
|
72
|
+
payload, header = JWT.decode(jwt, 'thisismysecret')
|
73
|
+
```
|
74
|
+
|
75
|
+
Decoding with RSA:
|
76
|
+
```ruby
|
77
|
+
# private key used to sign the token
|
78
|
+
private_key = OpenSSL::PKey::RSA.generate(512)
|
79
|
+
jwt = 'string containing real jwt token'
|
80
|
+
payload, header = JWT.decode(jwt, private_key.public_key, algorithm: 'RS256')
|
81
|
+
```
|
82
|
+
|
83
|
+
Decoding a token with claims:
|
84
|
+
```ruby
|
85
|
+
jwt = 'string containing real jwt token'
|
86
|
+
payload, header = JWT.decode(jwt, 'thisismysecret', claims: { aud: 'foo' })
|
87
|
+
```
|
88
|
+
|
89
|
+
###Supported Algorithms
|
90
|
+
|
91
|
+
Algorithm parameter | Algorithm used
|
92
|
+
----------|-------
|
93
|
+
HS256 | HMAC using SHA-256 algorithm
|
94
|
+
HS384 | HMAC using SHA-384 algorithm
|
95
|
+
HS512 | HMAC using SHA-512 algorithm
|
96
|
+
RS256 | RSA using SHA-256 algorithm
|
97
|
+
RS384 | RSA using SHA-384 algorithm
|
98
|
+
RS512 | RSA using SHA-512 algorithm
|
99
|
+
|
100
|
+
## Contributing
|
101
|
+
|
102
|
+
1. Fork it ( https://github.com/chupipandi/jwt-rb/fork )
|
103
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
104
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
105
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
106
|
+
5. Create a new Pull Request
|
107
|
+
|
108
|
+
## License
|
109
|
+
|
110
|
+
MIT
|
data/Rakefile
ADDED
data/jwtrb.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require File.expand_path('../lib/jwt/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = 'jwt-rb'
|
6
|
+
spec.version = JWT::VERSION
|
7
|
+
spec.authors = ["ChupipandiCrew"]
|
8
|
+
spec.email = ["chupi@chupipandi.com"]
|
9
|
+
spec.summary = %q{Ruby gem to encode/ decode JWT}
|
10
|
+
spec.homepage = "http://github.com/chupipandi/jwt-rb"
|
11
|
+
spec.license = "MIT"
|
12
|
+
|
13
|
+
spec.files = `git ls-files -z`.split("\x0")
|
14
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
15
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
16
|
+
spec.require_paths = ["lib"]
|
17
|
+
|
18
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
19
|
+
spec.add_development_dependency "rake"
|
20
|
+
spec.add_development_dependency 'rspec'
|
21
|
+
spec.add_development_dependency 'timecop'
|
22
|
+
end
|
data/lib/jwt/decoder.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
module JWT
|
2
|
+
class Decoder
|
3
|
+
include JWT::Verificator
|
4
|
+
include JWT::Signature
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def decode(jwt, key, options)
|
8
|
+
@algorithm = options[:algorithm] || 'HS256'
|
9
|
+
@jwt = jwt
|
10
|
+
|
11
|
+
validate_jwt!
|
12
|
+
|
13
|
+
header, payload, input, signature = decode_token
|
14
|
+
|
15
|
+
verify_claims!(payload, options[:claims])
|
16
|
+
verify_signature!(key, signature, input)
|
17
|
+
|
18
|
+
[payload, header]
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def validate_jwt!
|
24
|
+
if parse_token.length != 3
|
25
|
+
fail JWT::DecoderError.new('Invalid number of segments')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def parse_token
|
30
|
+
@jwt.split('.')
|
31
|
+
end
|
32
|
+
|
33
|
+
def decode_token
|
34
|
+
header, payload, signature = parse_token
|
35
|
+
|
36
|
+
input = [header, payload].join('.')
|
37
|
+
header = decode_header(header)
|
38
|
+
payload = Payload.new(decode_payload(payload))
|
39
|
+
signature = decode_signature(signature)
|
40
|
+
|
41
|
+
[header, payload, input, signature]
|
42
|
+
end
|
43
|
+
|
44
|
+
def decode_header(header)
|
45
|
+
fail JWT::DecoderError.new('Empty header') if header.empty?
|
46
|
+
header = base64_decode(header)
|
47
|
+
JSON.parse(header)
|
48
|
+
end
|
49
|
+
|
50
|
+
def decode_payload(payload)
|
51
|
+
fail JWT::DecoderError.new('Empty payload') if payload.empty?
|
52
|
+
payload = base64_decode(payload)
|
53
|
+
JSON.parse(payload)
|
54
|
+
end
|
55
|
+
|
56
|
+
def decode_signature(signature)
|
57
|
+
base64_decode(signature)
|
58
|
+
end
|
59
|
+
|
60
|
+
def base64_decode(string)
|
61
|
+
string += '=' * (4 - string.length.modulo(4))
|
62
|
+
Base64.decode64(string.tr('-_','+/'))
|
63
|
+
end
|
64
|
+
|
65
|
+
def verify_signature!(key, signature, input)
|
66
|
+
supported_algorithm!(@algorithm)
|
67
|
+
|
68
|
+
parsed_algorithm = parse_algorithm(@algorithm)
|
69
|
+
digest = generate_digest(parsed_algorithm)
|
70
|
+
|
71
|
+
if @algorithm =~ /^HS/
|
72
|
+
hs_key_format!(key)
|
73
|
+
signed_input = sign_hmac(digest, key, input)
|
74
|
+
|
75
|
+
unless secure_compare(signature, signed_input)
|
76
|
+
fail JWT::VerificationError.new('Signature does not match')
|
77
|
+
end
|
78
|
+
else
|
79
|
+
rs_key_format!(key)
|
80
|
+
verify_rsa(key, digest, signature, input)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# From devise & inspired from ruby-jwt of http://github.com/progrium
|
85
|
+
# constant-time comparison algorithm to prevent timing attacks
|
86
|
+
def secure_compare(a, b)
|
87
|
+
return false if a.nil? || b.nil? || a.empty? || b.empty? || a.bytesize != b.bytesize
|
88
|
+
l = a.unpack "C#{a.bytesize}"
|
89
|
+
|
90
|
+
res = 0
|
91
|
+
b.each_byte { |byte| res |= byte ^ l.shift }
|
92
|
+
res == 0
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
data/lib/jwt/encoder.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
module JWT
|
2
|
+
class Encoder
|
3
|
+
include JWT::Verificator
|
4
|
+
include JWT::Signature
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def encode(payload, key, options)
|
8
|
+
@algorithm = options[:algorithm] || 'HS256'
|
9
|
+
@payload = Payload.new(payload)
|
10
|
+
|
11
|
+
header = encode_header
|
12
|
+
payload = decorate_and_encode_payload(options[:claims] || {})
|
13
|
+
signature = encode_signature(header, payload, key)
|
14
|
+
|
15
|
+
[header, payload, signature].join('.')
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def encode_header
|
21
|
+
base64_encode(header)
|
22
|
+
end
|
23
|
+
|
24
|
+
def header
|
25
|
+
{ typ: 'JWT', alg: @algorithm }.to_json
|
26
|
+
end
|
27
|
+
|
28
|
+
def encode_payload
|
29
|
+
base64_encode(@payload.to_json)
|
30
|
+
end
|
31
|
+
|
32
|
+
def decorate_and_encode_payload(claims)
|
33
|
+
@payload[:iat] = Time.now.to_i
|
34
|
+
@payload[:exp] = @payload[:iat] + claims[:exp] if valid_integer_claim(claims[:exp])
|
35
|
+
@payload[:aud] = claims[:aud] if valid_string_claim(claims[:aud])
|
36
|
+
@payload[:iss] = claims[:iss] if valid_string_claim(claims[:iss])
|
37
|
+
|
38
|
+
encode_payload
|
39
|
+
end
|
40
|
+
|
41
|
+
def base64_encode(string)
|
42
|
+
Base64.encode64(string).tr('+/', '-_').gsub(/[\n=]/, '')
|
43
|
+
end
|
44
|
+
|
45
|
+
def encode_signature(header, payload, key)
|
46
|
+
input = [header, payload].join('.')
|
47
|
+
signature = sign(input, key)
|
48
|
+
|
49
|
+
base64_encode(signature)
|
50
|
+
end
|
51
|
+
|
52
|
+
def sign(input, key)
|
53
|
+
supported_algorithm!(@algorithm)
|
54
|
+
|
55
|
+
parsed_algorithm = parse_algorithm(@algorithm)
|
56
|
+
digest = generate_digest(parsed_algorithm)
|
57
|
+
|
58
|
+
if @algorithm =~ /^HS/
|
59
|
+
hs_key_format!(key)
|
60
|
+
sign_hmac(digest, key, input)
|
61
|
+
else
|
62
|
+
rs_key_format!(key)
|
63
|
+
sign_rsa(key, digest, input)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
data/lib/jwt/payload.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module JWT
|
2
|
+
module Signature
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def parse_algorithm(algorithm)
|
12
|
+
algorithm.sub(/^../, 'sha')
|
13
|
+
end
|
14
|
+
|
15
|
+
def generate_digest(parsed_algorithm)
|
16
|
+
OpenSSL::Digest.new(parsed_algorithm)
|
17
|
+
end
|
18
|
+
|
19
|
+
def sign_hmac(digest, key, input)
|
20
|
+
OpenSSL::HMAC.digest(digest, key, input)
|
21
|
+
end
|
22
|
+
|
23
|
+
def sign_rsa(private_key, digest, input)
|
24
|
+
private_key.sign(digest, input)
|
25
|
+
rescue
|
26
|
+
JWT::VerificationError.new('Your key needs to be able to .sign()')
|
27
|
+
end
|
28
|
+
|
29
|
+
def verify_rsa(public_key, digest, signature, input)
|
30
|
+
fail JWT::VerificationError unless public_key.verify(digest, signature, input)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module JWT
|
2
|
+
module Verificator
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
SUPPORTED_ALGS = %w(HS256 HS384 HS512 RS256 RS384 RS512)
|
9
|
+
private
|
10
|
+
|
11
|
+
def supported_algorithm!(algorithm)
|
12
|
+
if !SUPPORTED_ALGS.include? algorithm
|
13
|
+
fail NotImplementedError.new("#{algorithm} is not supported")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def hs_key_format!(key)
|
18
|
+
fail JWT::InvalidKeyFormatError unless key.is_a? String
|
19
|
+
end
|
20
|
+
|
21
|
+
def rs_key_format!(key)
|
22
|
+
fail JWT::InvalidKeyFormatError unless key.is_a? OpenSSL::PKey::RSA
|
23
|
+
end
|
24
|
+
|
25
|
+
def valid_integer_claim(claim)
|
26
|
+
claim.is_a? Integer
|
27
|
+
end
|
28
|
+
|
29
|
+
def valid_string_claim(claim)
|
30
|
+
claim.is_a? String
|
31
|
+
end
|
32
|
+
|
33
|
+
def verify_claims!(payload, options)
|
34
|
+
validate_expiration(payload[:exp]) if payload[:exp]
|
35
|
+
validate_audience(payload[:aud], options[:aud]) if payload[:aud]
|
36
|
+
validate_issuer(payload[:iss], options[:iss]) if payload[:iss]
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate_expiration(exp)
|
40
|
+
fail JWT::VerificationError.new('JWT Expired') if Time.now.to_i >= exp
|
41
|
+
end
|
42
|
+
|
43
|
+
def validate_audience(aud, given_aud)
|
44
|
+
if given_aud && given_aud != aud
|
45
|
+
fail JWT::VerificationError.new("Invalid Audience. Expected: #{aud}")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def validate_issuer(iss, given_iss)
|
50
|
+
if given_iss && given_iss != iss
|
51
|
+
fail JWT::VerificationError.new("Invalid Issuer. Expected: #{iss}")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
data/lib/jwt/version.rb
ADDED
data/lib/jwt.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'jwt/payload'
|
2
|
+
require 'jwt/verificator'
|
3
|
+
require 'jwt/signature'
|
4
|
+
require 'jwt/encoder'
|
5
|
+
require 'jwt/decoder'
|
6
|
+
require 'jwt/exceptions'
|
7
|
+
require 'json'
|
8
|
+
require 'base64'
|
9
|
+
require 'openssl'
|
10
|
+
|
11
|
+
module JWT
|
12
|
+
module ModuleFunctions
|
13
|
+
def encode(payload, key, options = {})
|
14
|
+
Encoder.encode(payload, key, options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def decode(jwt, key, options = {})
|
18
|
+
Decoder.decode(jwt, key, options)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
extend ModuleFunctions
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# All jwt used here have been created by external tools to isolate the tests
|
4
|
+
|
5
|
+
describe 'Decoder' do
|
6
|
+
before do
|
7
|
+
@payload = { 'hello' => 'world', 'iat' => 946681200 }
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'decoding' do
|
11
|
+
it 'decodes a valid JWT using HS' do
|
12
|
+
secret = 'mysecret'
|
13
|
+
jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIiwiaWF0Ijo5NDY2ODEyMDB9.9OzXVFu0LK4miYiSkWrP5vhzHVtz5DHC8dWalkTah3c'
|
14
|
+
payload, header = JWT.decode(jwt, secret)
|
15
|
+
|
16
|
+
expect(payload).to eq(@payload)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'decodes a valid JWT using RS' do
|
20
|
+
public_key = OpenSSL::PKey::RSA.new(<<-KEY)
|
21
|
+
-----BEGIN PUBLIC KEY-----
|
22
|
+
MFswDQYJKoZIhvcNAQEBBQADSgAwRwJAUTQbhg/Hcgq26wY9yOWu9L6OX4fqekqo
|
23
|
+
R+YhQUXaJGXhW0tNapTdIwTAycby0cUCXoA/7IYi4SXSKMT0owQxeQIDAQAB
|
24
|
+
-----END PUBLIC KEY-----
|
25
|
+
KEY
|
26
|
+
|
27
|
+
jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIiwiaWF0Ijo5NDY2ODEyMDB9.A0Qiqmo02gEviZtQW8ESC94oKLXcgCSGnktyxDQZNE0AfSoMJZ9JMEWNjEXyp6k7VEm9nn9ZbEu2GdmAIJz-xw'
|
28
|
+
payload, header = JWT.decode(jwt, public_key, algorithm: 'RS256')
|
29
|
+
|
30
|
+
expect(payload).to eq(@payload)
|
31
|
+
end
|
32
|
+
|
33
|
+
describe 'claims' do
|
34
|
+
it 'validates a JWT with the correct audience' do
|
35
|
+
expected_payload = { 'hello' => 'world', 'iat' => 946681200, 'aud' => 'rubyist' }
|
36
|
+
secret = 'mysecret'
|
37
|
+
jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIiwiaWF0Ijo5NDY2ODEyMDAsImF1ZCI6InJ1Ynlpc3QifQ.u6DB-dpK5zxZUmgprzXtg5yY4djsLLtIv4EAgE_nWSM'
|
38
|
+
|
39
|
+
payload, header = JWT.decode(jwt, secret, claims: { aud: 'rubyist' })
|
40
|
+
|
41
|
+
expect(payload).to eq(expected_payload)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'validates a JWT with expiry if it didnt expire yet' do
|
45
|
+
# The expiry is set to 24 hours
|
46
|
+
expected_payload = { 'hello' => 'world', 'iat' => 946681200, 'exp' => 946767600}
|
47
|
+
secret = 'mysecret'
|
48
|
+
jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIiwiaWF0Ijo5NDY2ODEyMDAsImV4cCI6OTQ2NzY3NjAwfQ.AZ9a_9L8oY95OT7jc019ZxjWc9fcT5chYsZy6QUgzj0'
|
49
|
+
|
50
|
+
# Travel 12 hours in the future
|
51
|
+
Timecop.travel(Time.at(946724400))
|
52
|
+
|
53
|
+
payload, header = JWT.decode(jwt, secret)
|
54
|
+
|
55
|
+
expect(payload).to eq(expected_payload)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'validates a JWT with the correct issuer' do
|
59
|
+
expected_payload = { 'hello' => 'world', 'iat' => 946681200, 'iss' => 'toni' }
|
60
|
+
secret = 'mysecret'
|
61
|
+
jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIiwiaWF0Ijo5NDY2ODEyMDAsImlzcyI6InRvbmkifQ.I4EoPQzt97rehCBnU8VTcDH5R2KX6b8Ta1t5bANGorc'
|
62
|
+
|
63
|
+
payload, header = JWT.decode(jwt, secret, claims: { iss: 'toni' })
|
64
|
+
|
65
|
+
expect(payload).to eq(expected_payload)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe 'exceptions' do
|
71
|
+
it 'raises an error if you use a string as a key in RS decoding' do
|
72
|
+
secret = 'mysecret'
|
73
|
+
jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIn0.E0f3M2R4p1_pEmDFp3PZNfXwLp6m3z7MNX468o8gQCYeslnOMdCGvzl1pCJhDs5M7KnaSlvPm_Be3WjYrk8ZDQ'
|
74
|
+
|
75
|
+
expect { JWT.decode(jwt, secret, algorithm: 'RS256') }
|
76
|
+
.to raise_error JWT::InvalidKeyFormatError
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'raises an error if you dont use a string as a key in HS decoding' do
|
80
|
+
key = OpenSSL::PKey::RSA.new(<<-KEY)
|
81
|
+
-----BEGIN PUBLIC KEY-----
|
82
|
+
MFswDQYJKoZIhvcNAQEBBQADSgAwRwJAUTQbhg/Hcgq26wY9yOWu9L6OX4fqekqo
|
83
|
+
R+YhQUXaJGXhW0tNapTdIwTAycby0cUCXoA/7IYi4SXSKMT0owQxeQIDAQAB
|
84
|
+
-----END PUBLIC KEY-----
|
85
|
+
KEY
|
86
|
+
|
87
|
+
jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIn0.E0f3M2R4p1_pEmDFp3PZNfXwLp6m3z7MNX468o8gQCYeslnOMdCGvzl1pCJhDs5M7KnaSlvPm_Be3WjYrk8ZDQ'
|
88
|
+
|
89
|
+
expect { JWT.decode(jwt, key) }
|
90
|
+
.to raise_error JWT::InvalidKeyFormatError
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'raises an error if the token doesnt have the correct number of segments' do
|
94
|
+
secret = 'mysecret'
|
95
|
+
jwt = 'header.payload.signature.hello?'
|
96
|
+
|
97
|
+
expect { JWT.decode(jwt, secret) }
|
98
|
+
.to raise_error JWT::DecoderError
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'raises an error if the token cant be verified with the current secret key' do
|
102
|
+
secret = 'myothersecret'
|
103
|
+
jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIn0.Gc_-AK7EN0nCQj6egXy525yk_cssK2A2lgX-w2NM90M'
|
104
|
+
|
105
|
+
expect { JWT.decode(jwt, secret) }
|
106
|
+
.to raise_error JWT::VerificationError
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'raises an error if there is no header' do
|
110
|
+
secret = 'myothersecret'
|
111
|
+
jwt = '.eyJoZWxsbyI6IndvcmxkIn0.Gc_-AK7EN0nCQj6egXy525yk_cssK2A2lgX-w2NM90M'
|
112
|
+
|
113
|
+
expect { JWT.decode(jwt, secret) }
|
114
|
+
.to raise_error JWT::DecoderError
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'raises an error if there is no payload' do
|
118
|
+
secret = 'myothersecret'
|
119
|
+
jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..Gc_-AK7EN0nCQj6egXy525yk_cssK2A2lgX-w2NM90M'
|
120
|
+
|
121
|
+
expect { JWT.decode(jwt, secret) }
|
122
|
+
.to raise_error JWT::DecoderError
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'raises an error if the audience doesnt match' do
|
126
|
+
secret = 'mysecret'
|
127
|
+
jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIiwiaWF0Ijo5NDY2ODEyMDAsImF1ZCI6InJ1Ynlpc3QifQ.u6DB-dpK5zxZUmgprzXtg5yY4djsLLtIv4EAgE_nWSM'
|
128
|
+
|
129
|
+
expect { JWT.decode(jwt, secret, claims: { aud: 'phpers' })}
|
130
|
+
.to raise_error JWT::VerificationError
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'raises an error if the issuer doesnt match' do
|
134
|
+
secret = 'mysecret'
|
135
|
+
jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIiwiaWF0Ijo5NDY2ODEyMDAsImlzcyI6InRvbmkifQ.I4EoPQzt97rehCBnU8VTcDH5R2KX6b8Ta1t5bANGorc'
|
136
|
+
|
137
|
+
expect { JWT.decode(jwt, secret, claims: { iss: 'john' })}
|
138
|
+
.to raise_error JWT::VerificationError
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'raises an error if the token expired' do
|
142
|
+
# The expiry is set to 24 hours
|
143
|
+
secret = 'mysecret'
|
144
|
+
jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIiwiaWF0Ijo5NDY2ODEyMDAsImV4cCI6OTQ2NzY3NjAwfQ.AZ9a_9L8oY95OT7jc019ZxjWc9fcT5chYsZy6QUgzj0'
|
145
|
+
|
146
|
+
# Travel 24 hours, 1 second in the future
|
147
|
+
Timecop.travel(Time.at(946767601))
|
148
|
+
|
149
|
+
expect { JWT.decode(jwt, secret) }
|
150
|
+
.to raise_error JWT::VerificationError
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# All expected_jwt used here have been created by external tools to isolate the tests
|
4
|
+
|
5
|
+
describe 'Encoder' do
|
6
|
+
before do
|
7
|
+
Timecop.freeze(Time.at(946681200))
|
8
|
+
@payload = { hello: 'world' }
|
9
|
+
end
|
10
|
+
|
11
|
+
describe 'encoding' do
|
12
|
+
it 'encodes a JWT using HS' do
|
13
|
+
secret = 'mysecret'
|
14
|
+
expected_jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIiwiaWF0Ijo5NDY2ODEyMDB9.9OzXVFu0LK4miYiSkWrP5vhzHVtz5DHC8dWalkTah3c'
|
15
|
+
jwt = JWT.encode(@payload, secret)
|
16
|
+
|
17
|
+
expect(jwt).to eq(expected_jwt)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'encodes a JWT using RS' do
|
21
|
+
key = OpenSSL::PKey::RSA.new(<<-KEY)
|
22
|
+
-----BEGIN RSA PRIVATE KEY-----
|
23
|
+
MIIBOAIBAAJAUTQbhg/Hcgq26wY9yOWu9L6OX4fqekqoR+YhQUXaJGXhW0tNapTd
|
24
|
+
IwTAycby0cUCXoA/7IYi4SXSKMT0owQxeQIDAQABAkBI9IfF6mdGDlpIzVK1K6YE
|
25
|
+
PS+spHAFbw3BiwBVpGxYRiumQrZyjKfpPpXw1dM9Qrm0Q3hXPLEzB3Ea/aw3rAAB
|
26
|
+
AiEAoGHNoRMeBnMC7mwbi8rf8RFriWpTp91IoFzD+oPV/fECIQCBnb3Tn4Uxccmd
|
27
|
+
2L+yoYMottUTVONq8l2YDBQlTG6ECQIgcLBHq0WjcySciqmrMS3664cx5/uti+UP
|
28
|
+
gp2rlfnMAgECIAfVedilho5Te0UQCZ4JRv0Z98zgT5JyLZf3+uu6L9/JAiBubUAY
|
29
|
+
c1CuIOv3cWDJPNMWI57s1U2WF+zbtIkc1zr5+Q==
|
30
|
+
-----END RSA PRIVATE KEY-----
|
31
|
+
KEY
|
32
|
+
jwt = JWT.encode(@payload, key, algorithm: 'RS256')
|
33
|
+
expected_jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIiwiaWF0Ijo5NDY2ODEyMDB9.A0Qiqmo02gEviZtQW8ESC94oKLXcgCSGnktyxDQZNE0AfSoMJZ9JMEWNjEXyp6k7VEm9nn9ZbEu2GdmAIJz-xw'
|
34
|
+
expect(jwt).to eq(expected_jwt)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'can use a payload with string as keys' do
|
38
|
+
payload = { 'hello' => 'world' }
|
39
|
+
secret = 'mysecret'
|
40
|
+
expected_jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIiwiaWF0Ijo5NDY2ODEyMDB9.9OzXVFu0LK4miYiSkWrP5vhzHVtz5DHC8dWalkTah3c'
|
41
|
+
jwt = JWT.encode(payload, secret)
|
42
|
+
|
43
|
+
expect(jwt).to eq(expected_jwt)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'can use old ruby hash style' do
|
47
|
+
payload = { :hello => 'world' }
|
48
|
+
secret = 'mysecret'
|
49
|
+
expected_jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIiwiaWF0Ijo5NDY2ODEyMDB9.9OzXVFu0LK4miYiSkWrP5vhzHVtz5DHC8dWalkTah3c'
|
50
|
+
jwt = JWT.encode(payload, secret)
|
51
|
+
|
52
|
+
expect(jwt).to eq(expected_jwt)
|
53
|
+
end
|
54
|
+
|
55
|
+
describe 'claims' do
|
56
|
+
it 'allows you to set an expiry time' do
|
57
|
+
secret = 'mysecret'
|
58
|
+
expected_jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIiwiaWF0Ijo5NDY2ODEyMDAsImV4cCI6OTQ2Njg3MjAwfQ.zbalHdCubQpn_U32jW1dv9L2p3vWSgf-UsKwnEiFxWo'
|
59
|
+
jwt = JWT.encode(@payload, secret, claims: { exp: 6000 })
|
60
|
+
|
61
|
+
expect(jwt).to eq(expected_jwt)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'allows you to set an audience' do
|
65
|
+
secret = 'mysecret'
|
66
|
+
expected_jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIiwiaWF0Ijo5NDY2ODEyMDAsImF1ZCI6InJ1Ynlpc3QifQ.u6DB-dpK5zxZUmgprzXtg5yY4djsLLtIv4EAgE_nWSM'
|
67
|
+
jwt = JWT.encode(@payload, secret, claims: { aud: 'rubyist' })
|
68
|
+
|
69
|
+
expect(jwt).to eq(expected_jwt)
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'allows you to set an issuer' do
|
73
|
+
secret = 'mysecret'
|
74
|
+
expected_jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJoZWxsbyI6IndvcmxkIiwiaWF0Ijo5NDY2ODEyMDAsImlzcyI6InRvbmkifQ.I4EoPQzt97rehCBnU8VTcDH5R2KX6b8Ta1t5bANGorc'
|
75
|
+
jwt = JWT.encode(@payload, secret, claims: { iss: 'toni' })
|
76
|
+
|
77
|
+
expect(jwt).to eq(expected_jwt)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe 'exceptions' do
|
83
|
+
it 'raises NotImplementedError using a fake signature' do
|
84
|
+
secret = 'mysecret'
|
85
|
+
expect { JWT.encode(@payload, secret, algorithm: 'HS9888889') }
|
86
|
+
.to raise_error NotImplementedError
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'raises an error if you dont use a private key on RS algorithms' do
|
90
|
+
secret = 'mysecret'
|
91
|
+
expect { JWT.encode(@payload, secret, algorithm: 'RS256') }
|
92
|
+
.to raise_error JWT::InvalidKeyFormatError
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jwt-rb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- ChupipandiCrew
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-07-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: timecop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
- chupi@chupipandi.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- ".travis.yml"
|
78
|
+
- Gemfile
|
79
|
+
- LICENSE.txt
|
80
|
+
- README.md
|
81
|
+
- Rakefile
|
82
|
+
- jwtrb.gemspec
|
83
|
+
- lib/jwt.rb
|
84
|
+
- lib/jwt/decoder.rb
|
85
|
+
- lib/jwt/encoder.rb
|
86
|
+
- lib/jwt/exceptions.rb
|
87
|
+
- lib/jwt/payload.rb
|
88
|
+
- lib/jwt/signature.rb
|
89
|
+
- lib/jwt/verificator.rb
|
90
|
+
- lib/jwt/version.rb
|
91
|
+
- spec/decoder_spec.rb
|
92
|
+
- spec/encoder_spec.rb
|
93
|
+
- spec/spec_helper.rb
|
94
|
+
homepage: http://github.com/chupipandi/jwt-rb
|
95
|
+
licenses:
|
96
|
+
- MIT
|
97
|
+
metadata: {}
|
98
|
+
post_install_message:
|
99
|
+
rdoc_options: []
|
100
|
+
require_paths:
|
101
|
+
- lib
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
requirements: []
|
113
|
+
rubyforge_project:
|
114
|
+
rubygems_version: 2.3.0
|
115
|
+
signing_key:
|
116
|
+
specification_version: 4
|
117
|
+
summary: Ruby gem to encode/ decode JWT
|
118
|
+
test_files:
|
119
|
+
- spec/decoder_spec.rb
|
120
|
+
- spec/encoder_spec.rb
|
121
|
+
- spec/spec_helper.rb
|