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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 32ba799c3c70c2a2d97847f43783f122061ca956
4
- data.tar.gz: 48e78024ba8183d174845324994d98e7a7bb2335
3
+ metadata.gz: 34e37b26161d949ec37dfa1699ac3c73bd16a6a6
4
+ data.tar.gz: 9fc1af9868b25087073f995f0dd497bbc936fd5e
5
5
  SHA512:
6
- metadata.gz: b1c1bfb51899743df81370e00ea4961eb5af410e90a10c9d1ab544547f8601b55a2e9aede318e590edb9033d4d0d8203904d303fa9fe10533641ea091745b848
7
- data.tar.gz: 24257ad9bc718101ce0fe7cbb5c5aaf7df7bdbae56106f7800f6f717291884f31ffe6f65126ac339aa4a62b09d20a493cb17c3d9c77a535bd5bbbc443c542ae1
6
+ metadata.gz: 41429c9b24ab68bcffc806e9c29c4b615a435237540d47e0f28aee7387259ddec92aa0ba68fd850b2f4902d682fb87a4737ec038668190ff9835f096472315f8
7
+ data.tar.gz: 99d552f23c11e324a3985fbf391aa115d42883257124f55517a887d20e1d71605a7eff24fbad192c83be8960b64740a4aefc6c4aab8d676230c52263acfe0e6f
@@ -1,2 +1,3 @@
1
- e ~v(LC6���86M@z�$b ���mP�$n�N����v��.$`٪�ɩi(e4�����"|��6΂��,��l5bְ�Ǵ�ϽE�B��\C.p{>�A�]Z/���]��q,Y�h���D5#��=���GԆP�ǕM�~����yi�>���z�
2
- *P��m`��W���?���C�ƒO~�۾!*���wm7������V�3�+�J��й��H)��MO���yX8��� 5[���b�:�袋
1
+ 1��F�M��ů�C �pv(�&�e�����\3����H4�-��}>����� �;�G~����ϱr�v�J�ֹ�.��q5�:��=�
2
+ o˘5�B 9pGPFtH 2DX�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 Rack::JsonWebTokenAuth.new do
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 => C::RackResponse
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, C::RackRequestHttpAuth)
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 = C::BEARER_TOKEN_REGEX.match(env['HTTP_AUTHORIZATION'])[1]
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 StandardError => e
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, C::ResourcePath, Hash => C::Any
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: C::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 C::ResourcePath => C::Maybe[Fixnum]
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 C::ResourcePath => Regexp
42
+ Contract String => Regexp
45
43
  def compile(path)
46
44
  if path.respond_to? :to_str
47
45
  special_chars = %w{. + ( )}
@@ -1,4 +1,3 @@
1
- require 'custom_contracts'
2
1
  require 'rack/json_web_token_auth/resource'
3
2
 
4
3
  module Rack
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  class JsonWebTokenAuth
3
- VERSION = '0.1.0'.freeze
3
+ VERSION = '0.1.1'.freeze
4
4
  end
5
5
  end
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.0
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
@@ -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