rack-cloudflare-jwt 0.2.0 → 0.3.0

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
  SHA256:
3
- metadata.gz: 9cfffcc56a02828c0ab0aea34ce64dd64e7fa09b9c564cc2146f7a54f01ff189
4
- data.tar.gz: 55b46d11820643dead91670a3c23aaa25d0d80526844ecdcadb38c2ec5110465
3
+ metadata.gz: 55cdfb2ba442bac6516a587a070c304d1a4ecab379cbb24a231fd80ddebf6e51
4
+ data.tar.gz: 96e59628339f522d60626d4e82635b2ea864c4d732835e6c20b58fcc6d7c12eb
5
5
  SHA512:
6
- metadata.gz: 637d37665fa3e39c8d65649ad3fde2bee0cd84a3bf1d3e8974e2abb49c2e3d051785f63dee1bc5ac914e601076e0cf14b6a0c04a359b675f7b3e8cd1cae7c294
7
- data.tar.gz: 8879652a99cf5639b2ad6543524ea4ff28f7ce92be470a4d6594f9554751b10f6e3079262532d2b9a7948cb277a7f33d6e06171f4e147d8898f24eeec0a079e8
6
+ metadata.gz: 636a1384bb904479c1fb7e126c919488a307af159651dc5b509ab39012fa01bb4d54af1fd50a3a1e4541e1a40c4be2810e3fda7c47e802774f4c37e101d4e113
7
+ data.tar.gz: d42e6f7ec8d19d16281b9df23daedc13116e8cec2fcd61f94324529197d3b13142d8e3662f3d393fe25924f848c65a5793d1a9eb67dd3391f811558ca7aa11a8
data/README.md CHANGED
@@ -48,6 +48,14 @@ Rails.application.config.middleware.use Rack::CloudflareJwt::Auth, 'my-team-doma
48
48
  '/my-path-2' => 'xxx.yyy.zzz',
49
49
  ```
50
50
 
51
+ # Releasing
52
+
53
+ ```
54
+ gem signin
55
+
56
+
57
+ ```
58
+
51
59
  ## Contributing
52
60
 
53
61
  1. Fork it ( https://github.com/Shuttlerock/rack-cloudflare-jwt/fork )
@@ -5,231 +5,229 @@ require 'multi_json'
5
5
  require 'net/http'
6
6
  require 'rack/jwt'
7
7
 
8
- module Rack::CloudflareJwt
9
- # Authentication middleware
10
- #
11
- # @see https://developers.cloudflare.com/access/setting-up-access/validate-jwt-tokens/
12
- class Auth
13
- # Custom decode token error.
14
- class DecodeTokenError < StandardError; end
15
-
16
- # Certs path
17
- CERTS_PATH = '/cdn-cgi/access/certs'
18
- # Default algorithm
19
- DEFAULT_ALGORITHM = 'RS256'
20
- # CloudFlare JWT header.
21
- HEADER_NAME = 'HTTP_CF_ACCESS_JWT_ASSERTION'
22
- # Key for get current path.
23
- PATH_INFO = 'PATH_INFO'
24
-
25
- # Token regex.
26
- #
27
- # @see https://github.com/jwt/ruby-jwt/tree/v2.2.1#algorithms-and-usage
28
- TOKEN_REGEX = /
29
- ^(
30
- [a-zA-Z0-9\-_]+\. # 1 or more chars followed by a single period
31
- [a-zA-Z0-9\-_]+\. # 1 or more chars followed by a single period
32
- [a-zA-Z0-9\-_]+ # 1 or more chars, no trailing chars
33
- )$
34
- /x.freeze
35
-
36
- attr_reader :policies, :team_domain
37
-
38
- # Initializes middleware
39
- #
40
- # @example Initialize middleware in Rails
41
- # config.middleware.use(
42
- # Rack::CloudflareJwt::Auth,
43
- # ENV['RACK_CLOUDFLARE_JWT_TEAM_DOMAIN'],
44
- # '/admin' => <cloudflare-aud-1>,
45
- # '/manager' => <cloudflare-aud-2>,
46
- # )
47
- #
48
- # @param team_domain [String] the Team Domain (e.g. 'test.cloudflareaccess.com').
49
- # @param policies [Hash<String, String>] the policies with paths and AUDs.
50
- def initialize(app, team_domain, policies = {})
51
- @app = app
52
- @team_domain = team_domain
53
- @policies = policies
54
-
55
- check_policy_auds!
56
- check_paths_type!
57
- end
8
+ # Authentication middleware
9
+ #
10
+ # @see https://developers.cloudflare.com/access/setting-up-access/validate-jwt-tokens/
11
+ class Rack::CloudflareJwt::Auth
12
+ # Custom decode token error.
13
+ class DecodeTokenError < StandardError; end
14
+
15
+ # Certs path
16
+ CERTS_PATH = '/cdn-cgi/access/certs'
17
+ # Default algorithm
18
+ DEFAULT_ALGORITHM = 'RS256'
19
+ # CloudFlare JWT header.
20
+ HEADER_NAME = 'HTTP_CF_ACCESS_JWT_ASSERTION'
21
+ # Key for get current path.
22
+ PATH_INFO = 'PATH_INFO'
23
+
24
+ # Token regex.
25
+ #
26
+ # @see https://github.com/jwt/ruby-jwt/tree/v2.2.1#algorithms-and-usage
27
+ TOKEN_REGEX = /
28
+ ^(
29
+ [a-zA-Z0-9\-_]+\. # 1 or more chars followed by a single period
30
+ [a-zA-Z0-9\-_]+\. # 1 or more chars followed by a single period
31
+ [a-zA-Z0-9\-_]+ # 1 or more chars, no trailing chars
32
+ )$
33
+ /x.freeze
34
+
35
+ attr_reader :policies, :team_domain
36
+
37
+ # Initializes middleware
38
+ #
39
+ # @example Initialize middleware in Rails
40
+ # config.middleware.use(
41
+ # Rack::CloudflareJwt::Auth,
42
+ # ENV['RACK_CLOUDFLARE_JWT_TEAM_DOMAIN'],
43
+ # '/admin' => <cloudflare-aud-1>,
44
+ # '/manager' => <cloudflare-aud-2>,
45
+ # )
46
+ #
47
+ # @param team_domain [String] the Team Domain (e.g. 'test.cloudflareaccess.com').
48
+ # @param policies [Hash<String, String>] the policies with paths and AUDs.
49
+ def initialize(app, team_domain, policies = {})
50
+ @app = app
51
+ @team_domain = team_domain
52
+ @policies = policies
53
+
54
+ check_policy_auds!
55
+ check_paths_type!
56
+ end
58
57
 
59
- # Public: Call a middleware.
60
- def call(env)
61
- if !path_matches?(env)
62
- @app.call(env)
63
- elsif missing_auth_header?(env)
64
- return_error('Missing Authorization header')
65
- elsif invalid_auth_header?(env)
66
- return_error('Invalid Authorization header format')
67
- else
68
- verify_token(env)
69
- end
58
+ # Public: Call a middleware.
59
+ def call(env)
60
+ if !path_matches?(env)
61
+ @app.call(env)
62
+ elsif missing_auth_header?(env)
63
+ return_error('Missing Authorization header')
64
+ elsif invalid_auth_header?(env)
65
+ return_error('Invalid Authorization header format')
66
+ else
67
+ verify_token(env)
70
68
  end
69
+ end
71
70
 
72
- private
71
+ private
73
72
 
74
- # Private: Check policy auds.
75
- def check_policy_auds!
76
- raise ArgumentError, 'policies cannot be nil/empty' if policies.values.empty?
73
+ # Private: Check policy auds.
74
+ def check_policy_auds!
75
+ raise ArgumentError, 'policies cannot be nil/empty' if policies.values.empty?
77
76
 
78
- policies.each_value do |policy_aud|
79
- next unless !policy_aud.is_a?(String) || policy_aud.strip.empty?
77
+ policies.each_value do |policy_aud|
78
+ next unless !policy_aud.is_a?(String) || policy_aud.strip.empty?
80
79
 
81
- raise ArgumentError, 'policy AUD argument cannot be nil/empty'
82
- end
80
+ raise ArgumentError, 'policy AUD argument cannot be nil/empty'
83
81
  end
82
+ end
84
83
 
85
- # Private: Check paths type.
86
- def check_paths_type!
87
- policies.each_key do |path|
88
- raise ArgumentError, 'each key element must be a String' unless path.is_a?(String)
89
- raise ArgumentError, 'each key element must not be empty' if path.empty?
90
- raise ArgumentError, 'each key element must start with a /' unless path.start_with?('/')
91
- end
84
+ # Private: Check paths type.
85
+ def check_paths_type!
86
+ policies.each_key do |path|
87
+ raise ArgumentError, 'each key element must be a String' unless path.is_a?(String)
88
+ raise ArgumentError, 'each key element must not be empty' if path.empty?
89
+ raise ArgumentError, 'each key element must start with a /' unless path.start_with?('/')
92
90
  end
91
+ end
93
92
 
94
- # Private: Verify a token.
95
- def verify_token(env)
96
- # extract the token from header.
97
- token = env[HEADER_NAME]
98
- policy_aud = policies.find { |path, _aud| env[PATH_INFO].start_with?(path) }&.last
99
- decoded_token = public_keys.find do |key|
100
- break decode_token(token, key.public_key, policy_aud)
101
- rescue DecodeTokenError => e
102
- logger.info e.message
103
- nil
104
- end
105
-
106
- if decoded_token
107
- logger.debug 'CloudFlare JWT token is valid'
108
-
109
- env['jwt.payload'] = decoded_token.first
110
- env['jwt.header'] = decoded_token.last
111
- @app.call(env)
112
- else
113
- return_error('Invalid token')
114
- end
93
+ # Private: Verify a token.
94
+ def verify_token(env)
95
+ # extract the token from header.
96
+ token = env[HEADER_NAME]
97
+ policy_aud = policies.find { |path, _aud| env[PATH_INFO].start_with?(path) }&.last
98
+ decoded_token = public_keys.find do |key|
99
+ break decode_token(token, key.public_key, policy_aud)
100
+ rescue DecodeTokenError => e
101
+ logger.info e.message
102
+ nil
115
103
  end
116
104
 
117
- # Private: Decode a token.
118
- #
119
- # @param token [String] the token.
120
- # @param secret [String] the public key.
121
- # @param policy_aud [String] the CloudFlare AUD.
122
- #
123
- # @example
124
- #
125
- # [
126
- # {"data"=>"test"}, # payload
127
- # {"alg"=>"RS256"} # header
128
- # ]
129
- #
130
- # @return [Array<Hash>] the token or `nil` at error.
131
- # @raise [DecodeTokenError] if the token is invalid.
132
- #
133
- # @see https://github.com/jwt/ruby-jwt/tree/v2.2.1#algorithms-and-usage
134
- def decode_token(token, secret, policy_aud)
135
- Rack::JWT::Token.decode(token, secret, true, aud: policy_aud, verify_aud: true, algorithm: DEFAULT_ALGORITHM)
136
- rescue ::JWT::VerificationError
137
- raise DecodeTokenError, 'Invalid JWT token : Signature Verification Error'
138
- rescue ::JWT::ExpiredSignature
139
- raise DecodeTokenError, 'Invalid JWT token : Expired Signature (exp)'
140
- rescue ::JWT::IncorrectAlgorithm
141
- raise DecodeTokenError, 'Invalid JWT token : Incorrect Key Algorithm'
142
- rescue ::JWT::ImmatureSignature
143
- raise DecodeTokenError, 'Invalid JWT token : Immature Signature (nbf)'
144
- rescue ::JWT::InvalidIssuerError
145
- raise DecodeTokenError, 'Invalid JWT token : Invalid Issuer (iss)'
146
- rescue ::JWT::InvalidIatError
147
- raise DecodeTokenError, 'Invalid JWT token : Invalid Issued At (iat)'
148
- rescue ::JWT::InvalidAudError
149
- raise DecodeTokenError, 'Invalid JWT token : Invalid Audience (aud)'
150
- rescue ::JWT::InvalidSubError
151
- raise DecodeTokenError, 'Invalid JWT token : Invalid Subject (sub)'
152
- rescue ::JWT::InvalidJtiError
153
- raise DecodeTokenError, 'Invalid JWT token : Invalid JWT ID (jti)'
154
- rescue ::JWT::DecodeError
155
- raise DecodeTokenError, 'Invalid JWT token : Decode Error'
156
- end
105
+ if decoded_token
106
+ logger.debug 'CloudFlare JWT token is valid'
157
107
 
158
- # Private: Check if current path is in the policies.
159
- #
160
- # @return [Boolean] true if it is, false otherwise.
161
- def path_matches?(env)
162
- policies.empty? || policies.keys.any? { |ex| env[PATH_INFO].start_with?(ex) }
108
+ env['jwt.payload'] = decoded_token.first
109
+ env['jwt.header'] = decoded_token.last
110
+ @app.call(env)
111
+ else
112
+ return_error('Invalid token')
163
113
  end
114
+ end
164
115
 
165
- # Private: Check if auth header is invalid.
166
- #
167
- # @return [Boolean] true if it is, false otherwise.
168
- def invalid_auth_header?(env)
169
- env[HEADER_NAME] !~ TOKEN_REGEX
170
- end
116
+ # Private: Decode a token.
117
+ #
118
+ # @param token [String] the token.
119
+ # @param secret [String] the public key.
120
+ # @param policy_aud [String] the CloudFlare AUD.
121
+ #
122
+ # @example
123
+ #
124
+ # [
125
+ # {"data"=>"test"}, # payload
126
+ # {"alg"=>"RS256"} # header
127
+ # ]
128
+ #
129
+ # @return [Array<Hash>] the token or `nil` at error.
130
+ # @raise [DecodeTokenError] if the token is invalid.
131
+ #
132
+ # @see https://github.com/jwt/ruby-jwt/tree/v2.2.1#algorithms-and-usage
133
+ def decode_token(token, secret, policy_aud)
134
+ Rack::JWT::Token.decode(token, secret, true, aud: policy_aud, verify_aud: true, algorithm: DEFAULT_ALGORITHM)
135
+ rescue ::JWT::VerificationError
136
+ raise DecodeTokenError, 'Invalid JWT token : Signature Verification Error'
137
+ rescue ::JWT::ExpiredSignature
138
+ raise DecodeTokenError, 'Invalid JWT token : Expired Signature (exp)'
139
+ rescue ::JWT::IncorrectAlgorithm
140
+ raise DecodeTokenError, 'Invalid JWT token : Incorrect Key Algorithm'
141
+ rescue ::JWT::ImmatureSignature
142
+ raise DecodeTokenError, 'Invalid JWT token : Immature Signature (nbf)'
143
+ rescue ::JWT::InvalidIssuerError
144
+ raise DecodeTokenError, 'Invalid JWT token : Invalid Issuer (iss)'
145
+ rescue ::JWT::InvalidIatError
146
+ raise DecodeTokenError, 'Invalid JWT token : Invalid Issued At (iat)'
147
+ rescue ::JWT::InvalidAudError
148
+ raise DecodeTokenError, 'Invalid JWT token : Invalid Audience (aud)'
149
+ rescue ::JWT::InvalidSubError
150
+ raise DecodeTokenError, 'Invalid JWT token : Invalid Subject (sub)'
151
+ rescue ::JWT::InvalidJtiError
152
+ raise DecodeTokenError, 'Invalid JWT token : Invalid JWT ID (jti)'
153
+ rescue ::JWT::DecodeError
154
+ raise DecodeTokenError, 'Invalid JWT token : Decode Error'
155
+ end
171
156
 
172
- # Private: Check if no auth header.
173
- #
174
- # @return [Boolean] true if it is, false otherwise.
175
- def missing_auth_header?(env)
176
- env[HEADER_NAME].nil? || env[HEADER_NAME].strip.empty?
177
- end
157
+ # Private: Check if current path is in the policies.
158
+ #
159
+ # @return [Boolean] true if it is, false otherwise.
160
+ def path_matches?(env)
161
+ policies.empty? || policies.keys.any? { |ex| env[PATH_INFO].start_with?(ex) }
162
+ end
178
163
 
179
- # Private: Return an error.
180
- def return_error(message)
181
- body = { error: message }.to_json
182
- headers = { 'Content-Type' => 'application/json' }
164
+ # Private: Check if auth header is invalid.
165
+ #
166
+ # @return [Boolean] true if it is, false otherwise.
167
+ def invalid_auth_header?(env)
168
+ env[HEADER_NAME] !~ TOKEN_REGEX
169
+ end
183
170
 
184
- [403, headers, [body]]
185
- end
171
+ # Private: Check if no auth header.
172
+ #
173
+ # @return [Boolean] true if it is, false otherwise.
174
+ def missing_auth_header?(env)
175
+ env[HEADER_NAME].nil? || env[HEADER_NAME].strip.empty?
176
+ end
186
177
 
187
- # Private: Get public keys.
188
- #
189
- # @return [Array<OpenSSL::PKey::RSA>] the public keys.
190
- def public_keys
191
- fetch_public_keys_cached.map do |jwk_data|
192
- ::JWT::JWK.import(jwk_data).keypair
193
- end
194
- end
178
+ # Private: Return an error.
179
+ def return_error(message)
180
+ body = { error: message }.to_json
181
+ headers = { 'Content-Type' => 'application/json' }
182
+
183
+ [403, headers, [body]]
184
+ end
195
185
 
196
- # Private: Fetch public keys.
197
- #
198
- # @return [Array<Hash>] the public keys.
199
- def fetch_public_keys
200
- json = Net::HTTP.get(team_domain, CERTS_PATH)
201
- json.empty? ? [] : MultiJson.load(json, symbolize_keys: true).fetch(:keys)
202
- rescue StandardError
203
- []
186
+ # Private: Get public keys.
187
+ #
188
+ # @return [Array<OpenSSL::PKey::RSA>] the public keys.
189
+ def public_keys
190
+ fetch_public_keys_cached.map do |jwk_data|
191
+ ::JWT::JWK.import(jwk_data).keypair
204
192
  end
193
+ end
194
+
195
+ # Private: Fetch public keys.
196
+ #
197
+ # @return [Array<Hash>] the public keys.
198
+ def fetch_public_keys
199
+ json = Net::HTTP.get(team_domain, CERTS_PATH)
200
+ json.empty? ? [] : MultiJson.load(json, symbolize_keys: true).fetch(:keys)
201
+ rescue StandardError
202
+ []
203
+ end
205
204
 
206
- # Private: Get cached public keys.
207
- #
208
- # Store a keys in the cache only 10 minutes.
209
- #
210
- # @return [Array<Hash>] the public keys.
211
- def fetch_public_keys_cached
212
- key = [self.class.name, '#secrets'].join('_')
213
-
214
- if defined? Rails
215
- Rails.cache.fetch(key, expires_in: 600) { fetch_public_keys }
216
- elsif defined? Padrino
217
- keys = Padrino.cache[key]
218
- keys || Padrino.cache.store(key, fetch_public_keys, expires: 600)
219
- else
220
- fetch_public_keys
221
- end
205
+ # Private: Get cached public keys.
206
+ #
207
+ # Store a keys in the cache only 10 minutes.
208
+ #
209
+ # @return [Array<Hash>] the public keys.
210
+ def fetch_public_keys_cached
211
+ key = [self.class.name, '#secrets'].join('_')
212
+
213
+ if defined? Rails
214
+ Rails.cache.fetch(key, expires_in: 600) { fetch_public_keys }
215
+ elsif defined? Padrino
216
+ keys = Padrino.cache[key]
217
+ keys || Padrino.cache.store(key, fetch_public_keys, expires: 600)
218
+ else
219
+ fetch_public_keys
222
220
  end
221
+ end
223
222
 
224
- # Private: Get a logger.
225
- #
226
- # @return [ActiveSupport::Logger] the logger.
227
- def logger
228
- if defined? Rails
229
- Rails.logger
230
- elsif defined? Padrino
231
- Padrino.logger
232
- end
223
+ # Private: Get a logger.
224
+ #
225
+ # @return [ActiveSupport::Logger] the logger.
226
+ def logger
227
+ if defined? Rails
228
+ Rails.logger
229
+ elsif defined? Padrino
230
+ Padrino.logger
233
231
  end
234
232
  end
235
233
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack # rubocop:disable Style/ClassAndModuleChildren
4
4
  module CloudflareJwt
5
- VERSION = '0.2.0'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-cloudflare-jwt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aleksei Vokhmin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-14 00:00:00.000000000 Z
11
+ date: 2023-06-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -126,16 +126,22 @@ dependencies:
126
126
  name: jwt
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - "~>"
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '2.2'
132
+ - - "<"
130
133
  - !ruby/object:Gem::Version
131
- version: 2.2.0
134
+ version: '2.7'
132
135
  type: :runtime
133
136
  prerelease: false
134
137
  version_requirements: !ruby/object:Gem::Requirement
135
138
  requirements:
136
- - - "~>"
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '2.2'
142
+ - - "<"
137
143
  - !ruby/object:Gem::Version
138
- version: 2.2.0
144
+ version: '2.7'
139
145
  - !ruby/object:Gem::Dependency
140
146
  name: multi_json
141
147
  requirement: !ruby/object:Gem::Requirement
@@ -195,7 +201,8 @@ files:
195
201
  homepage: https://github.com/Shuttlerock/rack-cloudflare-jwt
196
202
  licenses:
197
203
  - MIT
198
- metadata: {}
204
+ metadata:
205
+ rubygems_mfa_required: 'true'
199
206
  post_install_message:
200
207
  rdoc_options: []
201
208
  require_paths:
@@ -211,7 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
211
218
  - !ruby/object:Gem::Version
212
219
  version: '0'
213
220
  requirements: []
214
- rubygems_version: 3.0.1
221
+ rubygems_version: 3.0.3.1
215
222
  signing_key:
216
223
  specification_version: 4
217
224
  summary: Rack middleware that provides authentication based on CloudFlare JSON Web