rack-json_web_token_auth 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 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