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 +4 -4
- data/README.md +8 -0
- data/lib/rack/cloudflare_jwt/auth.rb +199 -201
- data/lib/rack/cloudflare_jwt/version.rb +1 -1
- metadata +15 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 55cdfb2ba442bac6516a587a070c304d1a4ecab379cbb24a231fd80ddebf6e51
|
4
|
+
data.tar.gz: 96e59628339f522d60626d4e82635b2ea864c4d732835e6c20b58fcc6d7c12eb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
71
|
+
private
|
73
72
|
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
79
|
-
|
77
|
+
policies.each_value do |policy_aud|
|
78
|
+
next unless !policy_aud.is_a?(String) || policy_aud.strip.empty?
|
80
79
|
|
81
|
-
|
82
|
-
end
|
80
|
+
raise ArgumentError, 'policy AUD argument cannot be nil/empty'
|
83
81
|
end
|
82
|
+
end
|
84
83
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
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
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
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
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
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
|
-
|
185
|
-
|
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
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
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
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
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
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
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
|
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.
|
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:
|
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.
|
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.
|
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
|