zaikio-jwt_auth 2.1.1 → 2.3.0

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