zaikio-jwt_auth 2.1.1 → 2.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 +21 -0
- data/lib/zaikio/jwt_auth/directory_cache.rb +18 -6
- data/lib/zaikio/jwt_auth/jwk.rb +1 -1
- data/lib/zaikio/jwt_auth/rack_middleware.rb +27 -0
- data/lib/zaikio/jwt_auth/token_data.rb +7 -24
- data/lib/zaikio/jwt_auth/version.rb +1 -1
- data/lib/zaikio/jwt_auth.rb +63 -8
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 69257f2c9dcf661babfc6b5600c0b4b724c32b4e92ffb6743e47a560e020a81c
|
4
|
+
data.tar.gz: 0b61afe2f4587ba84ac3e922bcb67024b8ec9f53b69cdbf155c17b4c556388d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7717ef3559a647592cca0568e09531fdd8854c61d2dcdfb64077faec8bf0bd31e3dc87336b62dd0fa6b6673cbc6de4dfc5fb94d3371e2c9598e295d18e4d8578
|
7
|
+
data.tar.gz: 82094718a024d29a023220560517c77d59bc29b7d4dc293419b23ee9204bd936f6d14e76c4b2698c95a71ca0f72bddbb6bc2387d70b801f85da86b2ad2b86b5f
|
data/README.md
CHANGED
@@ -134,6 +134,27 @@ class ResourcesControllerTest < ActionDispatch::IntegrationTest
|
|
134
134
|
end
|
135
135
|
```
|
136
136
|
|
137
|
+
### 8. Setup rack-attack for throttling
|
138
|
+
|
139
|
+
This gem ships with a rack middleware that should be used to throttle requests by app and/or subject. You can use the middleware with [rack-attack](https://github.com/rack/rack-attack) as described here:
|
140
|
+
|
141
|
+
```rb
|
142
|
+
# config/initializers/rack_attack.rb
|
143
|
+
|
144
|
+
MyApp::Application.config.middleware.insert_before Rack::Attack, Zaikio::JWTAuth::RackMiddleware
|
145
|
+
|
146
|
+
class Rack::Attack
|
147
|
+
Rack::Attack.throttled_response_retry_after_header = true
|
148
|
+
|
149
|
+
throttle("zaikio/by_app_sub", limit: 600, period: 1.minute) do |request|
|
150
|
+
next unless request.path.start_with?("/api/")
|
151
|
+
next unless request.env[Zaikio::JWTAuth::RackMiddleware::SUBJECT] # does not use zaikio JWT
|
152
|
+
|
153
|
+
"#{request.env[Zaikio::JWTAuth::RackMiddleware::AUDIENCE]}/#{request.env[Zaikio::JWTAuth::RackMiddleware::SUBJECT]}"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
137
158
|
## Advanced
|
138
159
|
|
139
160
|
### `only` and `except`
|
@@ -15,17 +15,29 @@ module Zaikio
|
|
15
15
|
BadResponseError = Class.new(StandardError)
|
16
16
|
|
17
17
|
class << self
|
18
|
+
# Retrieve some data from the Hub, reachable at `directory_path`. Attempts to
|
19
|
+
# retrieve data from a cache first (usually Redis, if configured). Caching can be
|
20
|
+
# skipped by setting an `:invalidate` option, or if the cached data is stale it
|
21
|
+
# will be refetched from the Hub anyway. Please note that this method can return
|
22
|
+
# `nil` if there is no cache available and the Hub is giving error responses or
|
23
|
+
# failures.
|
24
|
+
#
|
25
|
+
# @example Fetching revoked access token information
|
26
|
+
#
|
27
|
+
# DirectoryCache.fetch("api/v1/revoked_access_tokens.json")
|
28
|
+
#
|
29
|
+
# @returns Hash (in the happy path)
|
30
|
+
# @returns nil (if the cache is unavailable and the API is down)
|
18
31
|
def fetch(directory_path, options = {})
|
19
32
|
cache = Zaikio::JWTAuth.configuration.cache.read("zaikio::jwt_auth::#{directory_path}")
|
20
33
|
|
21
|
-
|
34
|
+
return reload_or_enqueue(directory_path) unless cache
|
22
35
|
|
23
|
-
|
24
|
-
new_values = reload_or_enqueue(directory_path)
|
25
|
-
return new_values || json["data"]
|
26
|
-
end
|
36
|
+
json = Oj.load(cache)
|
27
37
|
|
28
|
-
json[
|
38
|
+
if options[:invalidate] || cache_expired?(json, options[:expires_after])
|
39
|
+
reload_or_enqueue(directory_path)
|
40
|
+
end || json["data"]
|
29
41
|
end
|
30
42
|
|
31
43
|
def update(directory_path, options = {})
|
data/lib/zaikio/jwt_auth/jwk.rb
CHANGED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Zaikio
|
2
|
+
module JWTAuth
|
3
|
+
class RackMiddleware
|
4
|
+
AUDIENCE = "zaikio.jwt.audience".freeze
|
5
|
+
SUBJECT = "zaikio.jwt.subject".freeze
|
6
|
+
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
token_data = begin
|
13
|
+
Zaikio::JWTAuth.extract(env["HTTP_AUTHORIZATION"])
|
14
|
+
rescue JWT::ExpiredSignature, JWT::DecodeError
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
if token_data
|
19
|
+
env[AUDIENCE] = token_data.audience || :personal_token
|
20
|
+
env[SUBJECT] = token_data.subject
|
21
|
+
end
|
22
|
+
|
23
|
+
@app.call(env)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -47,36 +47,15 @@ module Zaikio
|
|
47
47
|
|
48
48
|
# scope_options is an array of objects with:
|
49
49
|
# scope, app_name (optional), except/only (array, optional), type (read, write, readwrite)
|
50
|
-
def scope_by_configurations?(
|
51
|
-
configuration = scope_configurations.find do |scope_configuration|
|
52
|
-
action_matches = action_matches_config?(scope_configuration, action_name)
|
53
|
-
|
54
|
-
if action_matches && scope_configuration[:if] && !context.instance_exec(&scope_configuration[:if])
|
55
|
-
false
|
56
|
-
elsif action_matches && scope_configuration[:unless] && context.instance_exec(&scope_configuration[:unless])
|
57
|
-
false
|
58
|
-
else
|
59
|
-
action_matches
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
50
|
+
def scope_by_configurations?(configuration, action_name)
|
63
51
|
return true unless configuration
|
64
52
|
|
65
53
|
scope?(configuration[:scopes], action_name, app_name: configuration[:app_name], type: configuration[:type])
|
66
54
|
end
|
67
55
|
|
68
|
-
def
|
69
|
-
if scope_configuration[:only]
|
70
|
-
Array(scope_configuration[:only]).any? { |a| a.to_s == action_name }
|
71
|
-
elsif scope_configuration[:except]
|
72
|
-
Array(scope_configuration[:except]).none? { |a| a.to_s == action_name }
|
73
|
-
else
|
74
|
-
true
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
def scope?(allowed_scopes, action_name, app_name: nil, type: nil)
|
56
|
+
def scope?(allowed_scopes, action_name, app_name: nil, type: nil, scope: nil) # rubocop:disable Metrics/AbcSize
|
79
57
|
app_name ||= Zaikio::JWTAuth.configuration.app_name
|
58
|
+
scope ||= self.scope
|
80
59
|
Array(allowed_scopes).map(&:to_s).any? do |allowed_scope|
|
81
60
|
scope.any? do |s|
|
82
61
|
parts = s.split(".")
|
@@ -95,6 +74,10 @@ module Zaikio
|
|
95
74
|
subject_match[5]
|
96
75
|
end
|
97
76
|
|
77
|
+
def subject
|
78
|
+
"#{subject_type}/#{subject_id}"
|
79
|
+
end
|
80
|
+
|
98
81
|
def on_behalf_of_id
|
99
82
|
subject_match[3]
|
100
83
|
end
|
data/lib/zaikio/jwt_auth.rb
CHANGED
@@ -6,11 +6,14 @@ require "zaikio/jwt_auth/configuration"
|
|
6
6
|
require "zaikio/jwt_auth/directory_cache"
|
7
7
|
require "zaikio/jwt_auth/jwk"
|
8
8
|
require "zaikio/jwt_auth/token_data"
|
9
|
+
require "zaikio/jwt_auth/rack_middleware"
|
9
10
|
require "zaikio/jwt_auth/engine"
|
10
11
|
require "zaikio/jwt_auth/test_helper"
|
11
12
|
|
12
13
|
module Zaikio
|
13
14
|
module JWTAuth
|
15
|
+
DOCS_LINK = "For more information check our docs: https://docs.zaikio.com/guide/oauth/scopes.html".freeze
|
16
|
+
|
14
17
|
class << self
|
15
18
|
attr_accessor :configuration
|
16
19
|
end
|
@@ -33,10 +36,14 @@ module Zaikio
|
|
33
36
|
def self.revoked_token_ids
|
34
37
|
return [] if mocked_jwt_payload
|
35
38
|
|
36
|
-
configuration.revoked_token_ids
|
39
|
+
return configuration.revoked_token_ids if configuration.revoked_token_ids
|
40
|
+
|
41
|
+
result = DirectoryCache.fetch(
|
37
42
|
"api/v1/revoked_access_tokens.json",
|
38
43
|
expires_after: 60.minutes
|
39
|
-
)
|
44
|
+
) || {}
|
45
|
+
|
46
|
+
result.fetch("revoked_token_ids", [])
|
40
47
|
end
|
41
48
|
|
42
49
|
def self.included(base)
|
@@ -124,14 +131,61 @@ module Zaikio
|
|
124
131
|
|
125
132
|
private
|
126
133
|
|
134
|
+
def find_scope_configuration(scope_configurations)
|
135
|
+
scope_configurations.find do |scope_configuration|
|
136
|
+
action_matches = action_matches_config?(scope_configuration)
|
137
|
+
|
138
|
+
if action_matches && scope_configuration[:if] && !instance_exec(&scope_configuration[:if])
|
139
|
+
false
|
140
|
+
elsif action_matches && scope_configuration[:unless] && instance_exec(&scope_configuration[:unless])
|
141
|
+
false
|
142
|
+
else
|
143
|
+
action_matches
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def action_matches_config?(scope_configuration)
|
149
|
+
if scope_configuration[:only]
|
150
|
+
Array(scope_configuration[:only]).any? { |a| a.to_s == action_name }
|
151
|
+
elsif scope_configuration[:except]
|
152
|
+
Array(scope_configuration[:except]).none? { |a| a.to_s == action_name }
|
153
|
+
else
|
154
|
+
true
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def required_scopes(token_data, configuration)
|
159
|
+
Array(configuration[:scopes]).flat_map do |allowed_scope|
|
160
|
+
%i[r w rw].filter_map do |type|
|
161
|
+
app_name = configuration[:app_name] || Zaikio::JWTAuth.configuration.app_name
|
162
|
+
full_scope = "#{app_name}.#{allowed_scope}.#{type}"
|
163
|
+
if token_data.scope?([allowed_scope], action_name, app_name: app_name, type: configuration[:type],
|
164
|
+
scope: [full_scope])
|
165
|
+
full_scope
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
127
171
|
def show_error_if_authorize_by_jwt_scopes_fails(token_data)
|
172
|
+
configuration = find_scope_configuration(self.class.authorize_by_jwt_scopes)
|
173
|
+
|
128
174
|
return if token_data.scope_by_configurations?(
|
129
|
-
|
130
|
-
action_name
|
131
|
-
self
|
175
|
+
configuration,
|
176
|
+
action_name
|
132
177
|
)
|
133
178
|
|
134
|
-
|
179
|
+
details = nil
|
180
|
+
|
181
|
+
if configuration
|
182
|
+
required_scopes = required_scopes(token_data, configuration)
|
183
|
+
|
184
|
+
details = "This endpoint requires one of the following scopes: #{required_scopes.join(', ')} but your " \
|
185
|
+
"access token only includes the following scopes: #{token_data.scope.join(', ')} - #{DOCS_LINK}"
|
186
|
+
end
|
187
|
+
|
188
|
+
render_error(["unpermitted_scope", details])
|
135
189
|
end
|
136
190
|
|
137
191
|
def show_error_if_authorize_by_jwt_subject_type_fails(token_data)
|
@@ -140,7 +194,8 @@ module Zaikio
|
|
140
194
|
return
|
141
195
|
end
|
142
196
|
|
143
|
-
render_error("unpermitted_subject"
|
197
|
+
render_error(["unpermitted_subject", "Expected Subject Type: #{self.class.authorize_by_jwt_subject_type} | "\
|
198
|
+
"Subject type from Access Token: #{token_data.subject_type} - #{DOCS_LINK}"])
|
144
199
|
end
|
145
200
|
|
146
201
|
def show_error_if_token_is_revoked(token_data)
|
@@ -150,7 +205,7 @@ module Zaikio
|
|
150
205
|
end
|
151
206
|
|
152
207
|
def render_error(error, status: :forbidden)
|
153
|
-
render(status: status, json: { "errors" =>
|
208
|
+
render(status: status, json: { "errors" => Array(error).compact })
|
154
209
|
end
|
155
210
|
|
156
211
|
def jwt_options
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zaikio-jwt_auth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- crispymtn
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2023-02-09 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activejob
|
@@ -88,6 +88,7 @@ files:
|
|
88
88
|
- lib/zaikio/jwt_auth/directory_cache.rb
|
89
89
|
- lib/zaikio/jwt_auth/engine.rb
|
90
90
|
- lib/zaikio/jwt_auth/jwk.rb
|
91
|
+
- lib/zaikio/jwt_auth/rack_middleware.rb
|
91
92
|
- lib/zaikio/jwt_auth/railtie.rb
|
92
93
|
- lib/zaikio/jwt_auth/test_helper.rb
|
93
94
|
- lib/zaikio/jwt_auth/token_data.rb
|