rack-cloudflare-jwt 0.2.0 → 0.3.0

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