rack-cloudflare-jwt 0.1.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: 240d66a2a123b06cf3625765ce2d7b4772c2a46f059fd62decc90acd4d9e68bc
4
- data.tar.gz: 9bf13c926defed079e9266752c64ab3b38d004e3665252b3dd7a663db77ae66c
3
+ metadata.gz: 55cdfb2ba442bac6516a587a070c304d1a4ecab379cbb24a231fd80ddebf6e51
4
+ data.tar.gz: 96e59628339f522d60626d4e82635b2ea864c4d732835e6c20b58fcc6d7c12eb
5
5
  SHA512:
6
- metadata.gz: 735971f62a1c16c83d6591baa3d60c052107ef61076850a0598c2456c386aec32c62b79927e15560f2d9f47ef17f991ff084ee6bb27284ab17195e6fcd805148
7
- data.tar.gz: c3f15c032fa1715e728e6e0337ddfae61165a8bda598cc69f077b5f87f71e93d2e24b249ae79ac197361a533ac730cf8b1c0823f2f1b00e794b15b3d1180c41d
6
+ metadata.gz: 636a1384bb904479c1fb7e126c919488a307af159651dc5b509ab39012fa01bb4d54af1fd50a3a1e4541e1a40c4be2810e3fda7c47e802774f4c37e101d4e113
7
+ data.tar.gz: d42e6f7ec8d19d16281b9df23daedc13116e8cec2fcd61f94324529197d3b13142d8e3662f3d393fe25924f848c65a5793d1a9eb67dd3391f811558ca7aa11a8
data/README.md CHANGED
@@ -38,11 +38,22 @@ $ gem install rack-cloudflare-jwt
38
38
 
39
39
  * `Hash` value : `String` : A Application Audience (AUD) Tag.
40
40
 
41
+ Also, you should provide a Team Domain.
41
42
 
42
43
  ### Rails
43
44
 
44
45
  ```ruby
45
- Rails.application.config.middleware.use Rack::CloudflareJwt::Auth, '/my-path' => 'xxx.yyy.zzz'
46
+ Rails.application.config.middleware.use Rack::CloudflareJwt::Auth, 'my-team-domain.cloudflareaccess.com',
47
+ '/my-path-1' => 'aaa.bbb.ccc'
48
+ '/my-path-2' => 'xxx.yyy.zzz',
49
+ ```
50
+
51
+ # Releasing
52
+
53
+ ```
54
+ gem signin
55
+
56
+
46
57
  ```
47
58
 
48
59
  ## Contributing
@@ -5,235 +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
- # HTTP_HOST header.
23
- HEADER_HTTP_HOST = 'HTTP_HOST'
24
- # Key for get current path.
25
- PATH_INFO = 'PATH_INFO'
26
-
27
- # Token regex.
28
- #
29
- # @see https://github.com/jwt/ruby-jwt/tree/v2.2.1#algorithms-and-usage
30
- TOKEN_REGEX = /
31
- ^(
32
- [a-zA-Z0-9\-_]+\. # 1 or more chars followed by a single period
33
- [a-zA-Z0-9\-_]+\. # 1 or more chars followed by a single period
34
- [a-zA-Z0-9\-_]+ # 1 or more chars, no trailing chars
35
- )$
36
- /x.freeze
37
-
38
- attr_reader :policies
39
-
40
- # Initializes middleware
41
- #
42
- # @example Initialize middleware in Rails
43
- # config.middleware.use(
44
- # Rack::CloudflareJwt::Auth,
45
- # '/admin' => <cloudflare-aud-1>,
46
- # '/manager' => <cloudflare-aud-2>,
47
- # )
48
- #
49
- # @param policies [Hash<String, String>] the policies with paths and AUDs.
50
- def initialize(app, policies = {})
51
- @app = app
52
- @policies = policies
53
-
54
- check_policy_auds!
55
- check_paths_type!
56
- 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
57
57
 
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)
68
- 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)
69
68
  end
69
+ end
70
70
 
71
- private
71
+ private
72
72
 
73
- # Private: Check policy auds.
74
- def check_policy_auds!
75
- 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?
76
76
 
77
- policies.each_value do |policy_aud|
78
- 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?
79
79
 
80
- raise ArgumentError, 'policy AUD argument cannot be nil/empty'
81
- end
80
+ raise ArgumentError, 'policy AUD argument cannot be nil/empty'
82
81
  end
82
+ end
83
83
 
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?('/')
90
- 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?('/')
91
90
  end
91
+ end
92
92
 
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(env).find do |key|
99
- break decode_token(token, key.public_key, policy_aud)
100
- rescue DecodeTokenError => e
101
- logger.info e.message
102
- nil
103
- end
104
-
105
- if decoded_token
106
- logger.debug 'CloudFlare JWT token is valid'
107
-
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')
113
- 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
114
103
  end
115
104
 
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
105
+ if decoded_token
106
+ logger.debug 'CloudFlare JWT token is valid'
156
107
 
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) }
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')
162
113
  end
114
+ end
163
115
 
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
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
170
156
 
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
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
177
163
 
178
- # Private: Return an error.
179
- def return_error(message)
180
- body = { error: message }.to_json
181
- 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
182
170
 
183
- [403, headers, [body]]
184
- 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
185
177
 
186
- # Private: Get public keys.
187
- #
188
- # @return [Array<OpenSSL::PKey::RSA>] the public keys.
189
- def public_keys(env)
190
- host = env[HEADER_HTTP_HOST]
191
- fetch_public_keys_cached(host).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
- # @param host [String] The host.
199
- #
200
- # @return [Array<Hash>] the public keys.
201
- def fetch_public_keys(host)
202
- json = Net::HTTP.get(host, CERTS_PATH)
203
- json.empty? ? [] : MultiJson.load(json, symbolize_keys: true).fetch(:keys)
204
- rescue StandardError
205
- []
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
206
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
207
204
 
208
- # Private: Get cached public keys.
209
- #
210
- # Store a keys in the cache only 10 minutes.
211
- #
212
- # @param host [String] The host.
213
- #
214
- # @return [Array<Hash>] the public keys.
215
- def fetch_public_keys_cached(host)
216
- key = [self.class.name, '#secrets', host].join('_')
217
-
218
- if defined? Rails
219
- Rails.cache.fetch(key, expires_in: 600) { fetch_public_keys(host) }
220
- elsif defined? Padrino
221
- keys = Padrino.cache[key]
222
- keys || Padrino.cache.store(key, fetch_public_keys(host), expires: 600)
223
- else
224
- fetch_public_keys(host)
225
- 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
226
220
  end
221
+ end
227
222
 
228
- # Private: Get a logger.
229
- #
230
- # @return [ActiveSupport::Logger] the logger.
231
- def logger
232
- if defined? Rails
233
- Rails.logger
234
- elsif defined? Padrino
235
- Padrino.logger
236
- 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
237
231
  end
238
232
  end
239
233
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack # rubocop:disable Style/ClassAndModuleChildren
4
4
  module CloudflareJwt
5
- VERSION = '0.1.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.1.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-03-10 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.3
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