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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f9c9c81be4267790236a02e7b3ffe5b6f7e1aa8ac13196e421b6140cb109081
4
- data.tar.gz: 9081893a8669d3729bba2ccc7f8bc03eeb71ca371c637fddfc6d245d1ad0cfd1
3
+ metadata.gz: 69257f2c9dcf661babfc6b5600c0b4b724c32b4e92ffb6743e47a560e020a81c
4
+ data.tar.gz: 0b61afe2f4587ba84ac3e922bcb67024b8ec9f53b69cdbf155c17b4c556388d6
5
5
  SHA512:
6
- metadata.gz: 4c57d8b69bf85297feaf2486fe809de05c2cc2a2deed4a1123b9a27a4503b39d71182ff8cf8f2550e9ee9105729957e8d3395ec223ad146fa258c08132da0b31
7
- data.tar.gz: dabda6c4b3aa7ed7c1ffe41b0e14d40994db16918c85a9e86535fb7deb369af37226752b5e74ff925cb3e31d2919336066b9f1d626a3619c0dbfefe967d602ab
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
- json = Oj.load(cache) if cache
34
+ return reload_or_enqueue(directory_path) unless cache
22
35
 
23
- if !cache || options[:invalidate] || cache_expired?(json, options[:expires_after])
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["data"]
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 = {})
@@ -30,7 +30,7 @@ module Zaikio
30
30
  def keys
31
31
  return Zaikio::JWTAuth.configuration.keys if Zaikio::JWTAuth.configuration.keys
32
32
 
33
- fetch_from_cache["keys"]
33
+ fetch_from_cache.fetch("keys")
34
34
  end
35
35
 
36
36
  def fetch_from_cache(options = {})
@@ -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?(scope_configurations, action_name, context) # rubocop:disable Metrics/AbcSize
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 action_matches_config?(scope_configuration, action_name)
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
@@ -1,5 +1,5 @@
1
1
  module Zaikio
2
2
  module JWTAuth
3
- VERSION = "2.1.1".freeze
3
+ VERSION = "2.3.0".freeze
4
4
  end
5
5
  end
@@ -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 || DirectoryCache.fetch(
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
- )["revoked_token_ids"]
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
- self.class.authorize_by_jwt_scopes,
130
- action_name,
131
- self
175
+ configuration,
176
+ action_name
132
177
  )
133
178
 
134
- render_error("unpermitted_scope")
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" => [error] })
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.1.1
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: 2022-09-06 00:00:00.000000000 Z
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