rack-json_web_token_auth 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 +4 -4
- checksums.yaml.gz.sig +3 -2
- data.tar.gz.sig +0 -0
- data/README.md +2 -1
- data/lib/rack/json_web_token_auth.rb +22 -12
- data/lib/rack/json_web_token_auth/contracts.rb +95 -0
- data/lib/rack/json_web_token_auth/resource.rb +4 -6
- data/lib/rack/json_web_token_auth/resources.rb +0 -1
- data/lib/rack/json_web_token_auth/version.rb +1 -1
- metadata +2 -2
- metadata.gz.sig +0 -0
- data/lib/custom_contracts.rb +0 -135
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 34e37b26161d949ec37dfa1699ac3c73bd16a6a6
|
4
|
+
data.tar.gz: 9fc1af9868b25087073f995f0dd497bbc936fd5e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41429c9b24ab68bcffc806e9c29c4b615a435237540d47e0f28aee7387259ddec92aa0ba68fd850b2f4902d682fb87a4737ec038668190ff9835f096472315f8
|
7
|
+
data.tar.gz: 99d552f23c11e324a3985fbf391aa115d42883257124f55517a887d20e1d71605a7eff24fbad192c83be8960b64740a4aefc6c4aab8d676230c52263acfe0e6f
|
checksums.yaml.gz.sig
CHANGED
@@ -1,2 +1,3 @@
|
|
1
|
-
e
|
2
|
-
|
1
|
+
1��F�M��ů�C�pv(�&�e�����\3����H4�-��}>����� �;�G~����ϱr�v�J�ֹ�.��q5�:��=�
|
2
|
+
�o˘5�B�9�pGPFt�H 2�DX�d���U�
|
3
|
+
� �ʈ���[O%A�lC-FJJE�GQp��0���n�e%$���9�
|
data.tar.gz.sig
CHANGED
Binary file
|
data/README.md
CHANGED
@@ -115,7 +115,8 @@ gem and the resource path matching code is a direct port from it.
|
|
115
115
|
```ruby
|
116
116
|
require 'rack/json_web_token_auth'
|
117
117
|
|
118
|
-
use
|
118
|
+
# Sinatra `use` syntax
|
119
|
+
use Rack::JsonWebTokenAuth do
|
119
120
|
|
120
121
|
# You can define JWT options for all `secured` resources globally
|
121
122
|
# or you can specify a hash like this inside each block. If you want to
|
@@ -3,11 +3,14 @@ require 'contracts'
|
|
3
3
|
require 'hashie'
|
4
4
|
require 'jwt_claims'
|
5
5
|
|
6
|
+
require 'rack/json_web_token_auth/contracts'
|
6
7
|
require 'rack/json_web_token_auth/resources'
|
7
8
|
require 'rack/json_web_token_auth/resource'
|
8
|
-
require 'custom_contracts'
|
9
9
|
|
10
10
|
module Rack
|
11
|
+
# Custom error class
|
12
|
+
class TokenError < StandardError; end
|
13
|
+
|
11
14
|
# Rack Middleware for JSON Web Token Authentication
|
12
15
|
class JsonWebTokenAuth
|
13
16
|
include Contracts::Core
|
@@ -41,28 +44,28 @@ module Rack
|
|
41
44
|
all_resources << resources
|
42
45
|
end
|
43
46
|
|
44
|
-
Contract Hash =>
|
47
|
+
Contract Hash => RackResponse
|
45
48
|
def call(env)
|
46
49
|
begin
|
47
50
|
resource = resource_for_path(env[PATH_INFO_HEADER_KEY])
|
48
51
|
|
49
|
-
if resource.public_resource?
|
52
|
+
if resource && resource.public_resource?
|
50
53
|
# whitelisted as `unsecured`. skip all token authentication.
|
51
54
|
@app.call(env)
|
52
55
|
elsif resource.nil?
|
53
56
|
# no matching `secured` or `unsecured` resource.
|
54
57
|
# fail-safe with 401 unauthorized
|
55
|
-
raise 'No resource for path defined. Deny by default.'
|
58
|
+
raise TokenError, 'No resource for path defined. Deny by default.'
|
56
59
|
else
|
57
60
|
# a `secured` resource, validate the token to see if authenticated
|
58
61
|
|
59
62
|
# Test that `env` has a well formed Authorization header
|
60
|
-
unless Contract.valid?(env,
|
61
|
-
raise 'malformed Authorization header or token'
|
63
|
+
unless Contract.valid?(env, RackRequestHttpAuth)
|
64
|
+
raise TokenError, 'malformed Authorization header or token'
|
62
65
|
end
|
63
66
|
|
64
67
|
# Extract the token from the 'Authorization: Bearer token' string
|
65
|
-
token =
|
68
|
+
token = BEARER_TOKEN_REGEX.match(env['HTTP_AUTHORIZATION'])[1]
|
66
69
|
|
67
70
|
# Verify the token and its claims are valid
|
68
71
|
jwt_opts = resource.opts[:jwt]
|
@@ -77,25 +80,32 @@ module Rack
|
|
77
80
|
env[ENV_KEY] = Hashie.stringify_keys(jwt[:ok])
|
78
81
|
elsif Contract.valid?(jwt, C::HashOf[error: C::ArrayOf[Symbol]])
|
79
82
|
# a list of any registered claims that fail validation, if the JWT MAC is verified
|
80
|
-
raise "invalid JWT claims : #{jwt[:error].sort.join(', ')}"
|
83
|
+
raise TokenError, "invalid JWT claims : #{jwt[:error].sort.join(', ')}"
|
81
84
|
elsif Contract.valid?(jwt, C::HashOf[error: 'invalid JWT'])
|
82
85
|
# the JWT MAC is not verified
|
83
|
-
raise 'invalid JWT'
|
86
|
+
raise TokenError, 'invalid JWT'
|
84
87
|
elsif Contract.valid?(jwt, C::HashOf[error: 'invalid input'])
|
85
88
|
# otherwise
|
86
|
-
raise 'invalid JWT input'
|
89
|
+
raise TokenError, 'invalid JWT input'
|
87
90
|
else
|
88
|
-
raise 'unhandled JWT error'
|
91
|
+
raise TokenError, 'unhandled JWT error'
|
89
92
|
end
|
90
93
|
|
91
94
|
@app.call(env)
|
92
95
|
end
|
93
|
-
rescue
|
96
|
+
rescue TokenError => e
|
94
97
|
body = e.message.nil? ? 'Unauthorized' : "Unauthorized : #{e.message}"
|
95
98
|
headers = { 'WWW-Authenticate' => 'Bearer error="invalid_token"',
|
96
99
|
'Content-Type' => 'text/plain',
|
97
100
|
'Content-Length' => body.bytesize.to_s }
|
98
101
|
[401, headers, [body]]
|
102
|
+
rescue StandardError => e
|
103
|
+
# puts e.message
|
104
|
+
body = 'Unauthorized'
|
105
|
+
headers = { 'WWW-Authenticate' => 'Bearer error="invalid_token"',
|
106
|
+
'Content-Type' => 'text/plain',
|
107
|
+
'Content-Length' => body.bytesize.to_s }
|
108
|
+
[401, headers, [body]]
|
99
109
|
end
|
100
110
|
end
|
101
111
|
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Rack
|
2
|
+
class JsonWebTokenAuth
|
3
|
+
include Contracts::Core
|
4
|
+
C = Contracts
|
5
|
+
|
6
|
+
# Custom Contracts
|
7
|
+
# See : https://egonschiele.github.io/contracts.ruby/
|
8
|
+
|
9
|
+
# The last segment gets dropped for 'none' algorithm since there is no
|
10
|
+
# signature so both of these patterns are valid. All character chunks
|
11
|
+
# are base64url format and periods.
|
12
|
+
# Bearer abc123.abc123.abc123
|
13
|
+
# Bearer abc123.abc123.
|
14
|
+
BEARER_TOKEN_REGEX = %r{
|
15
|
+
^Bearer\s{1}( # starts with Bearer and a single space
|
16
|
+
[a-zA-Z0-9\-\_]+\. # 1 or more chars followed by a single period
|
17
|
+
[a-zA-Z0-9\-\_]+\. # 1 or more chars followed by a single period
|
18
|
+
[a-zA-Z0-9\-\_]* # 0 or more chars, no trailing chars
|
19
|
+
)$
|
20
|
+
}x
|
21
|
+
|
22
|
+
class RackRequestHttpAuth
|
23
|
+
def self.valid?(val)
|
24
|
+
Contract.valid?(val, ({ 'HTTP_AUTHORIZATION' => BEARER_TOKEN_REGEX }))
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.to_s
|
28
|
+
'A Rack request with JWT auth header'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class RackResponse
|
33
|
+
def self.valid?(val)
|
34
|
+
Contract.valid?(val, [C::Int, Hash, C::Any])
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.to_s
|
38
|
+
'A Rack response'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Key
|
43
|
+
def self.valid?(val)
|
44
|
+
return false if val.is_a?(String) && val.strip.empty?
|
45
|
+
C::Or[String, OpenSSL::PKey::RSA, OpenSSL::PKey::EC].valid?(val)
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.to_s
|
49
|
+
'A JWT secret string or signature key'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Algorithm
|
54
|
+
def self.valid?(val)
|
55
|
+
C::Enum['none', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512'].valid?(val)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.to_s
|
59
|
+
'A valid JWT token signature algorithm, or none'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class DecodedToken
|
64
|
+
def self.valid?(val)
|
65
|
+
C::ArrayOf[Hash].valid?(val) &&
|
66
|
+
C::DecodedTokenClaims.valid?(val[0]) &&
|
67
|
+
C::DecodedTokenHeader.valid?(val[1])
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.to_s
|
71
|
+
'A valid Array of decoded token claims and header Hashes'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class DecodedTokenClaims
|
76
|
+
def self.valid?(val)
|
77
|
+
C::HashOf[C::Or[String, Symbol] => C::Maybe[C::Or[String, C::Num, C::Bool, C::ArrayOf[C::Any], Hash]]].valid?(val)
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.to_s
|
81
|
+
'A valid decoded token payload attribute'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class DecodedTokenHeader
|
86
|
+
def self.valid?(val)
|
87
|
+
C::HashOf[C::Enum['typ', 'alg'] => C::Or['JWT', C::TokenAlgorithm]].valid?(val)
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.to_s
|
91
|
+
'A valid decoded token header attribute'
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'custom_contracts'
|
2
|
-
|
3
1
|
module Rack
|
4
2
|
class JsonWebTokenAuth
|
5
3
|
class Resource
|
@@ -8,7 +6,7 @@ module Rack
|
|
8
6
|
|
9
7
|
attr_accessor :public_resource, :path, :pattern, :opts
|
10
8
|
|
11
|
-
Contract C::Bool,
|
9
|
+
Contract C::Bool, String, Hash => C::Any
|
12
10
|
def initialize(public_resource, path, opts = {})
|
13
11
|
@public_resource = public_resource
|
14
12
|
@path = path
|
@@ -23,13 +21,13 @@ module Rack
|
|
23
21
|
else
|
24
22
|
# secured resources must have a :jwt hash with a :key
|
25
23
|
unless Contract.valid?(@opts, ({ jwt: { key: nil, alg: 'none' } })) ||
|
26
|
-
Contract.valid?(@opts, ({ jwt: { key:
|
24
|
+
Contract.valid?(@opts, ({ jwt: { key: Key } }))
|
27
25
|
raise 'invalid or missing jwt options for secured resource'
|
28
26
|
end
|
29
27
|
end
|
30
28
|
end
|
31
29
|
|
32
|
-
Contract
|
30
|
+
Contract String => C::Maybe[Fixnum]
|
33
31
|
def matches_path?(path)
|
34
32
|
pattern =~ path
|
35
33
|
end
|
@@ -41,7 +39,7 @@ module Rack
|
|
41
39
|
|
42
40
|
protected
|
43
41
|
|
44
|
-
Contract
|
42
|
+
Contract String => Regexp
|
45
43
|
def compile(path)
|
46
44
|
if path.respond_to? :to_str
|
47
45
|
special_chars = %w{. + ( )}
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-json_web_token_auth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Glenn Rempe
|
@@ -196,8 +196,8 @@ extra_rdoc_files: []
|
|
196
196
|
files:
|
197
197
|
- LICENSE.txt
|
198
198
|
- README.md
|
199
|
-
- lib/custom_contracts.rb
|
200
199
|
- lib/rack/json_web_token_auth.rb
|
200
|
+
- lib/rack/json_web_token_auth/contracts.rb
|
201
201
|
- lib/rack/json_web_token_auth/resource.rb
|
202
202
|
- lib/rack/json_web_token_auth/resources.rb
|
203
203
|
- lib/rack/json_web_token_auth/version.rb
|
metadata.gz.sig
CHANGED
Binary file
|
data/lib/custom_contracts.rb
DELETED
@@ -1,135 +0,0 @@
|
|
1
|
-
module Contracts
|
2
|
-
C = Contracts
|
3
|
-
|
4
|
-
# Custom Contracts
|
5
|
-
# See : https://egonschiele.github.io/contracts.ruby/
|
6
|
-
|
7
|
-
# The last segment gets dropped for 'none' algorithm since there is no
|
8
|
-
# signature so both of these patterns are valid. All character chunks
|
9
|
-
# are base64url format and periods.
|
10
|
-
# Bearer abc123.abc123.abc123
|
11
|
-
# Bearer abc123.abc123.
|
12
|
-
BEARER_TOKEN_REGEX = %r{
|
13
|
-
^Bearer\s{1}( # starts with Bearer and a single space
|
14
|
-
[a-zA-Z0-9\-\_]+\. # 1 or more chars followed by a single period
|
15
|
-
[a-zA-Z0-9\-\_]+\. # 1 or more chars followed by a single period
|
16
|
-
[a-zA-Z0-9\-\_]* # 0 or more chars, no trailing chars
|
17
|
-
)$
|
18
|
-
}x
|
19
|
-
|
20
|
-
class RackRequestHttpAuth
|
21
|
-
def self.valid?(val)
|
22
|
-
Contract.valid?(val, ({ 'HTTP_AUTHORIZATION' => BEARER_TOKEN_REGEX }))
|
23
|
-
end
|
24
|
-
|
25
|
-
def self.to_s
|
26
|
-
'A Rack request with JWT auth header'
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
class RackResponse
|
31
|
-
def self.valid?(val)
|
32
|
-
Contract.valid?(val, [C::Int, Hash, C::Any])
|
33
|
-
end
|
34
|
-
|
35
|
-
def self.to_s
|
36
|
-
'A Rack response'
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
class Key
|
41
|
-
def self.valid?(val)
|
42
|
-
return false if val.is_a?(String) && val.strip.empty?
|
43
|
-
C::Or[String, OpenSSL::PKey::RSA, OpenSSL::PKey::EC].valid?(val)
|
44
|
-
end
|
45
|
-
|
46
|
-
def self.to_s
|
47
|
-
'A JWT secret string or signature key'
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
class Algorithm
|
52
|
-
def self.valid?(val)
|
53
|
-
C::Enum['none', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512'].valid?(val)
|
54
|
-
end
|
55
|
-
|
56
|
-
def self.to_s
|
57
|
-
'A valid JWT token signature algorithm, or none'
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
class VerifierOptions
|
62
|
-
def self.valid?(val)
|
63
|
-
C::KeywordArgs[
|
64
|
-
key: C::Optional[C::Key],
|
65
|
-
alg: C::Optional[C::Algorithm],
|
66
|
-
iat: C::Optional[C::Int],
|
67
|
-
nbf: C::Optional[C::Int],
|
68
|
-
exp: C::Optional[C::Int],
|
69
|
-
iss: C::Optional[String],
|
70
|
-
jti: C::Optional[String],
|
71
|
-
aud: C::Optional[C::Or[String, C::ArrayOf[String], Symbol, C::ArrayOf[Symbol]]],
|
72
|
-
sub: C::Optional[String],
|
73
|
-
leeway_seconds: C::Optional[C::Int]
|
74
|
-
].valid?(val)
|
75
|
-
end
|
76
|
-
|
77
|
-
def self.to_s
|
78
|
-
'A Hash of token verifier options'
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
# abc123.abc123.abc123 (w/ signature)
|
83
|
-
# abc123.abc123. ('none')
|
84
|
-
class EncodedToken
|
85
|
-
def self.valid?(val)
|
86
|
-
val =~ /\A([a-zA-Z0-9\-\_]+\.[a-zA-Z0-9\-\_]+\.[a-zA-Z0-9\-\_]*)\z/
|
87
|
-
end
|
88
|
-
|
89
|
-
def self.to_s
|
90
|
-
'A valid encoded token'
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
class DecodedToken
|
95
|
-
def self.valid?(val)
|
96
|
-
C::ArrayOf[Hash].valid?(val) &&
|
97
|
-
C::DecodedTokenClaims.valid?(val[0]) &&
|
98
|
-
C::DecodedTokenHeader.valid?(val[1])
|
99
|
-
end
|
100
|
-
|
101
|
-
def self.to_s
|
102
|
-
'A valid Array of decoded token claims and header Hashes'
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
class DecodedTokenClaims
|
107
|
-
def self.valid?(val)
|
108
|
-
C::HashOf[C::Or[String, Symbol] => C::Maybe[C::Or[String, C::Num, C::Bool, C::ArrayOf[C::Any], Hash]]].valid?(val)
|
109
|
-
end
|
110
|
-
|
111
|
-
def self.to_s
|
112
|
-
'A valid decoded token payload attribute'
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
class DecodedTokenHeader
|
117
|
-
def self.valid?(val)
|
118
|
-
C::HashOf[C::Enum['typ', 'alg'] => C::Or['JWT', C::TokenAlgorithm]].valid?(val)
|
119
|
-
end
|
120
|
-
|
121
|
-
def self.to_s
|
122
|
-
'A valid decoded token header attribute'
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
class ResourcePath
|
127
|
-
def self.valid?(val)
|
128
|
-
C::Or[String, Regexp]
|
129
|
-
end
|
130
|
-
|
131
|
-
def self.to_s
|
132
|
-
'A valid resource path string or regex'
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|