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 +7 -0
- data/LICENSE +21 -0
- data/README.md +54 -0
- data/lib/rack/cloudflare_jwt/auth.rb +183 -0
- data/lib/rack/cloudflare_jwt/version.rb +7 -0
- data/lib/rack/cloudflare_jwt.rb +10 -0
- metadata +190 -0
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
|
+
[](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
|
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: []
|