rack-cloudflare-jwt 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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: []