rack-cloudflare-jwt 0.0.4

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 52042fd199a754f3e17a8af8f7c2d8199027bf8756dd28c0f198e70cea25588b
4
+ data.tar.gz: 37b5ee08766e991637676b22e722ec73d2936a7aeeb872bf9f7b6bad6a270064
5
+ SHA512:
6
+ metadata.gz: 4595f288d91508cd7f6744aae41599e4b0396a4d555077cf6dc9315c18ab4e1124e456643701346d275f4d7859a4ecf6d9c38b18a4511f0a83b15fd0badf8e3e
7
+ data.tar.gz: 1411676c2c0df41fffbae2ba8288e09c6061c6011a7162363524b6f3e0b0cdbaef8502c54810b5cc38d71864c7d73ec9bb4c2a468ebc1595b4c306ec9a87d6c4
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Shuttlerock
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Rack::CloudflareJwt
2
+
3
+ [![CircleCI](https://circleci.com/gh/Shuttlerock/rack-cloudflare-jwt.svg?style=svg)](https://circleci.com/gh/Shuttlerock/rack-cloudflare-jwt)
4
+
5
+ ## About
6
+
7
+ This gem provides CloudFlare JSON Web Token (JWT) based authentication.
8
+
9
+ ## Requirements
10
+
11
+ - Ruby 2.6.0 or greater
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's `Gemfile`:
16
+
17
+ ```ruby
18
+ gem 'rack-cloudflare-jwt'
19
+ ```
20
+
21
+ And then execute:
22
+
23
+ ```
24
+ $ bundle install
25
+ ```
26
+
27
+ Or install it directly with:
28
+
29
+ ```
30
+ $ gem install rack-cloudflare-jwt
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ `Rack::CloudflareJwt::Auth` accepts several configuration options. All options are passed in a single Ruby Hash:
36
+
37
+ * `policy_aud` : required : `String` : A Application Audience (AUD) Tag.
38
+
39
+ * `include_paths` : optional : Array : An Array of path strings representing paths that should be checked for the presence of a valid JWT token. Includes sub-paths as of specified paths as well (e.g. `%w(/docs)` includes `/docs/some/thing.html` also). Each path should start with a `/`. If a path not matches the current request path this entire middleware is skipped and no authentication or verification of tokens takes place.
40
+
41
+ ### Rails
42
+
43
+ ```ruby
44
+ require 'rack/cloudflare_jwt'
45
+ Rails.application.config.middleware.use Rack::CloudflareJwt::Auth, policy_aud: 'xxx.yyy.zzz', include_paths: %w[/foo]
46
+ ```
47
+
48
+ ## Contributing
49
+
50
+ 1. Fork it ( https://github.com/Shuttlerock/rack-cloudflare-jwt/fork )
51
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
52
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
53
+ 4. Push to the branch (`git push origin my-new-feature`)
54
+ 5. Create a new Pull Request
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt'
4
+ require 'multi_json'
5
+ require 'net/http'
6
+ require 'rack/jwt'
7
+
8
+ module Rack
9
+ module CloudflareJwt
10
+ # Authentication middleware
11
+ #
12
+ # @see https://developers.cloudflare.com/access/setting-up-access/validate-jwt-tokens/
13
+ class Auth
14
+ # Certs path
15
+ CERTS_PATH = '/cdn-cgi/access/certs'
16
+ # Default algorithm
17
+ DEFAULT_ALGORITHM = 'RS256'
18
+ # CloudFlare JWT header.
19
+ HEADER_NAME = 'HTTP_CF_ACCESS_JWT_ASSERTION'
20
+
21
+ # Token regex.
22
+ #
23
+ # @see https://github.com/jwt/ruby-jwt/tree/v2.2.1#algorithms-and-usage
24
+ TOKEN_REGEX = /
25
+ ^(
26
+ [a-zA-Z0-9\-\_]+\. # 1 or more chars followed by a single period
27
+ [a-zA-Z0-9\-\_]+\. # 1 or more chars followed by a single period
28
+ [a-zA-Z0-9\-\_]+ # 1 or more chars, no trailing chars
29
+ )$
30
+ /x.freeze
31
+
32
+ attr_reader :policy_aud, :include_paths
33
+
34
+ # Initializes middleware
35
+ def initialize(app, opts = {})
36
+ @app = app
37
+ @policy_aud = opts.fetch(:policy_aud, nil)
38
+ @include_paths = opts.fetch(:include_paths, [])
39
+
40
+ check_policy_aud!
41
+ check_include_paths_type!
42
+ end
43
+
44
+ # Public: Call a middleware.
45
+ def call(env)
46
+ if !path_matches_include_paths?(env)
47
+ @app.call(env)
48
+ elsif missing_auth_header?(env)
49
+ return_error('Missing Authorization header')
50
+ elsif invalid_auth_header?(env)
51
+ return_error('Invalid Authorization header format')
52
+ else
53
+ verify_token(env)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ # Private: Check policy aud.
60
+ def check_policy_aud!
61
+ return unless !policy_aud.is_a?(String) || policy_aud.strip.empty?
62
+
63
+ raise ArgumentError, 'policy_aud argument cannot be nil/empty'
64
+ end
65
+
66
+ # Private: Check include_paths type.
67
+ def check_include_paths_type!
68
+ raise ArgumentError, 'include_paths argument must be an Array' unless include_paths.is_a?(Array)
69
+
70
+ include_paths.each do |path|
71
+ raise ArgumentError, 'each include_paths Array element must be a String' unless path.is_a?(String)
72
+ raise ArgumentError, 'each include_paths Array element must not be empty' if path.empty?
73
+ raise ArgumentError, 'each include_paths Array element must start with a /' unless path.start_with?('/')
74
+ end
75
+ end
76
+
77
+ # Private: Verify a token.
78
+ def verify_token(env)
79
+ # extract the token from header.
80
+ token = env[HEADER_NAME]
81
+ decoded_token = public_keys(env).find do |key|
82
+ dt = decode_token(token, key.public_key)
83
+ break dt if dt
84
+ end
85
+
86
+ if decoded_token
87
+ env['jwt.payload'] = decoded_token.first
88
+ env['jwt.header'] = decoded_token.last
89
+ @app.call(env)
90
+ else
91
+ return_error('Invalid token')
92
+ end
93
+ end
94
+
95
+ # Private: Decode a token.
96
+ #
97
+ # Example:
98
+ #
99
+ # [
100
+ # {"data"=>"test"}, # payload
101
+ # {"alg"=>"RS256"} # header
102
+ # ]
103
+ #
104
+ # @return [Array<Hash>] the token.
105
+ # @see https://github.com/jwt/ruby-jwt/tree/v2.2.1#algorithms-and-usage
106
+ def decode_token(token, secret)
107
+ Rack::JWT::Token.decode(token, secret, true, aud: policy_aud, verify_aud: true, algorithm: DEFAULT_ALGORITHM)
108
+ rescue ::JWT::VerificationError
109
+ Rails.logger.info 'Invalid JWT token : Signature Verification Error'
110
+ rescue ::JWT::ExpiredSignature
111
+ Rails.logger.info 'Invalid JWT token : Expired Signature (exp)'
112
+ rescue ::JWT::IncorrectAlgorithm
113
+ Rails.logger.info 'Invalid JWT token : Incorrect Key Algorithm'
114
+ rescue ::JWT::ImmatureSignature
115
+ Rails.logger.info 'Invalid JWT token : Immature Signature (nbf)'
116
+ rescue ::JWT::InvalidIssuerError
117
+ Rails.logger.info 'Invalid JWT token : Invalid Issuer (iss)'
118
+ rescue ::JWT::InvalidIatError
119
+ Rails.logger.info 'Invalid JWT token : Invalid Issued At (iat)'
120
+ rescue ::JWT::InvalidAudError
121
+ Rails.logger.info 'Invalid JWT token : Invalid Audience (aud)'
122
+ rescue ::JWT::InvalidSubError
123
+ Rails.logger.info 'Invalid JWT token : Invalid Subject (sub)'
124
+ rescue ::JWT::InvalidJtiError
125
+ Rails.logger.info 'Invalid JWT token : Invalid JWT ID (jti)'
126
+ rescue ::JWT::DecodeError
127
+ Rails.logger.info 'Invalid JWT token : Decode Error'
128
+ end
129
+
130
+ # Private: Check if current path is in the include_paths.
131
+ #
132
+ # @return [Boolean] true if it is, false otherwise.
133
+ def path_matches_include_paths?(env)
134
+ include_paths.empty? || include_paths.any? { |ex| env['PATH_INFO'].start_with?(ex) }
135
+ end
136
+
137
+ # Private: Check if auth header is invalid.
138
+ #
139
+ # @return [Boolean] true if it is, false otherwise.
140
+ def invalid_auth_header?(env)
141
+ env[HEADER_NAME] !~ TOKEN_REGEX
142
+ end
143
+
144
+ # Private: Check if no auth header.
145
+ #
146
+ # @return [Boolean] true if it is, false otherwise.
147
+ def missing_auth_header?(env)
148
+ env[HEADER_NAME].nil? || env[HEADER_NAME].strip.empty?
149
+ end
150
+
151
+ # Private: Return an error.
152
+ def return_error(message)
153
+ body = { error: message }.to_json
154
+ headers = { 'Content-Type' => 'application/json' }
155
+
156
+ [403, headers, [body]]
157
+ end
158
+
159
+ # Private: Get public keys.
160
+ #
161
+ # @return [Array<OpenSSL::PKey::RSA>] the public keys.
162
+ def public_keys(env)
163
+ host = env['HTTP_HOST']
164
+ keys = Rails.cache.fetch([self.class.name, '#secrets', host]) { fetch_public_keys(host) }
165
+ keys.map do |jwk_data|
166
+ ::JWT::JWK.import(jwk_data).keypair
167
+ end
168
+ end
169
+
170
+ # Private: Fetch public keys.
171
+ #
172
+ # @param host [String] The host.
173
+ #
174
+ # @return [Array<Hash>] the public keys.
175
+ def fetch_public_keys(host)
176
+ json = Net::HTTP.get(host, CERTS_PATH)
177
+ json.present? ? MultiJson.load(json, symbolize_keys: true).fetch(:keys) : []
178
+ rescue StandardError
179
+ []
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ module CloudflareJwt
5
+ VERSION = '0.0.4'
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack/cloudflare_jwt/version'
4
+
5
+ module Rack
6
+ # CloudFlare JSON Web Token
7
+ module CloudflareJwt
8
+ autoload :Auth, 'rack/cloudflare_jwt/auth'
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,190 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-cloudflare-jwt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Aleksei Vokhmin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-01-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.16.2
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.16.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack-test
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.0.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 1.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 12.0.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 12.0.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.8.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 3.8.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop-performance
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 1.0.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 1.0.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 0.16.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 0.16.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: jwt
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 2.2.0
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 2.2.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: multi_json
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rack
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rack-jwt
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: 0.16.0
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: 0.16.0
153
+ description: Rack middleware that provides authentication based on CloudFlare JSON
154
+ Web Tokens.
155
+ email:
156
+ - avokhmin@gmail.com
157
+ executables: []
158
+ extensions: []
159
+ extra_rdoc_files: []
160
+ files:
161
+ - LICENSE
162
+ - README.md
163
+ - lib/rack/cloudflare_jwt.rb
164
+ - lib/rack/cloudflare_jwt/auth.rb
165
+ - lib/rack/cloudflare_jwt/version.rb
166
+ homepage: https://github.com/Shuttlerock/rack-cloudflare-jwt
167
+ licenses:
168
+ - MIT
169
+ metadata: {}
170
+ post_install_message:
171
+ rdoc_options: []
172
+ require_paths:
173
+ - lib
174
+ required_ruby_version: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - ">="
177
+ - !ruby/object:Gem::Version
178
+ version: 2.6.0
179
+ required_rubygems_version: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - ">="
182
+ - !ruby/object:Gem::Version
183
+ version: '0'
184
+ requirements: []
185
+ rubygems_version: 3.0.3
186
+ signing_key:
187
+ specification_version: 4
188
+ summary: Rack middleware that provides authentication based on CloudFlare JSON Web
189
+ Tokens.
190
+ test_files: []