clowk 0.6.1 → 0.7.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 +42 -0
- data/lib/clowk/authenticable.rb +102 -16
- data/lib/clowk/configuration.rb +12 -0
- data/lib/clowk/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 66d644459bae62c4a73b869fadc50402bfbe6fb060c0c6fc5ee521361a852be7
|
|
4
|
+
data.tar.gz: 1e81f740b072795cb9a84c2013f406ee5ea34a9bf5c1c92797cea9efd1c22d4e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 824a01a8708d3b4f1805c83acc20d2161c9e72f3be7c47ae9e1c2d9d80bbff624292d20e65cf03b53ca5384874caef3f1d038482cd4d4593cc688537b63a2d31
|
|
7
|
+
data.tar.gz: d4703e671a924025452c08686d6134f85563108d0edeb18bb049f9f9e4d119228d404ba3eccddae21840b2b63267c416aebac1b4a7f925689a9e37cd642cbf95
|
data/README.md
CHANGED
|
@@ -227,6 +227,48 @@ What changes in that mode:
|
|
|
227
227
|
session, keyed by a digest of the token. Without it, `enforce_active_session`
|
|
228
228
|
would cost a round trip to Clowk on every authenticated request.
|
|
229
229
|
|
|
230
|
+
### Keeping a session honest
|
|
231
|
+
|
|
232
|
+
A valid token proves who signed in, not that the session still stands —
|
|
233
|
+
revocation lives server-side. `config.enforce_active_session = true` checks it,
|
|
234
|
+
and `config.session_status_ttl` decides how often that costs a round trip.
|
|
235
|
+
|
|
236
|
+
Some actions cannot take a cached answer. Rotating a secret, deleting an
|
|
237
|
+
account, removing a member: a status from fourteen minutes ago is a hole. Name
|
|
238
|
+
those actions and they get a live one:
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
class ApiKeysController < ApplicationController
|
|
242
|
+
clowk_require_fresh_session only: [:create, :update, :destroy]
|
|
243
|
+
end
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
It takes the same options as `before_action`. Everything not named keeps the
|
|
247
|
+
cached check, so an app pays for the round trip on the few actions it cannot
|
|
248
|
+
undo and nowhere else. `clowk_enforce_fresh_session!` is the same thing as a
|
|
249
|
+
method, and `clowk_session_active?(force: true)` returns the answer instead of
|
|
250
|
+
enforcing it.
|
|
251
|
+
|
|
252
|
+
Two settings decide what happens when Clowk itself cannot be reached:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
config.fail_open_on_broker_error = true # default
|
|
256
|
+
config.max_session_age = 12.hours # default nil
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Failing open leaves the session standing and checks again on the next request —
|
|
260
|
+
a blip on the way to a single droplet must not sign everyone out. Only network
|
|
261
|
+
failures count; anything else still raises, because a bug must not read as "the
|
|
262
|
+
session is probably fine".
|
|
263
|
+
|
|
264
|
+
`max_session_age` is the other half of that, and failing open is not safe
|
|
265
|
+
without it: a local ceiling the broker plays no part in, so a permanently
|
|
266
|
+
unreachable Clowk cannot keep a session alive forever. Past it the session ends
|
|
267
|
+
with no round trip at all.
|
|
268
|
+
|
|
269
|
+
Both ends — the broker said inactive, the ceiling passed — go through
|
|
270
|
+
`config.on_session_expired` when you set one.
|
|
271
|
+
|
|
230
272
|
### Token verification
|
|
231
273
|
|
|
232
274
|
Tokens signed with `RS256` are verified against Clowk's public key set, fetched
|
data/lib/clowk/authenticable.rb
CHANGED
|
@@ -7,6 +7,11 @@ module Clowk
|
|
|
7
7
|
module Authenticable
|
|
8
8
|
extend ActiveSupport::Concern
|
|
9
9
|
|
|
10
|
+
# What a liveness check can raise once Clowk::Http's retries are spent, plus
|
|
11
|
+
# connection-refused, which retries never cover. Anything else is a bug and
|
|
12
|
+
# must not be swallowed into "the session is probably fine".
|
|
13
|
+
BROKER_UNAVAILABLE = [SystemCallError, Timeout::Error, IOError, SocketError, EOFError].freeze
|
|
14
|
+
|
|
10
15
|
def self.install_dynamic_methods(base)
|
|
11
16
|
scope = Clowk.config.prefix_by.to_s
|
|
12
17
|
current_method = :"current_#{scope}"
|
|
@@ -14,6 +19,7 @@ module Clowk
|
|
|
14
19
|
signed_in_method = :"#{scope}_signed_in?"
|
|
15
20
|
|
|
16
21
|
enforce_session_method = :"#{scope}_enforce_session!"
|
|
22
|
+
enforce_fresh_method = :"#{scope}_enforce_fresh_session!"
|
|
17
23
|
sign_out_method = :"#{scope}_sign_out!"
|
|
18
24
|
|
|
19
25
|
base.class_eval do
|
|
@@ -41,6 +47,12 @@ module Clowk
|
|
|
41
47
|
end
|
|
42
48
|
end
|
|
43
49
|
|
|
50
|
+
unless enforce_fresh_method == :clowk_enforce_fresh_session!
|
|
51
|
+
define_method(enforce_fresh_method) do
|
|
52
|
+
clowk_enforce_fresh_session!
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
44
56
|
unless sign_out_method == :clowk_sign_out!
|
|
45
57
|
define_method(sign_out_method) do
|
|
46
58
|
clowk_sign_out!
|
|
@@ -55,6 +67,22 @@ module Clowk
|
|
|
55
67
|
Clowk::Authenticable.install_dynamic_methods(self)
|
|
56
68
|
end
|
|
57
69
|
|
|
70
|
+
class_methods do
|
|
71
|
+
# Demand a live answer from Clowk before these actions, whatever a cached
|
|
72
|
+
# status says.
|
|
73
|
+
#
|
|
74
|
+
# class ApiKeysController < ApplicationController
|
|
75
|
+
# clowk_require_fresh_session only: [:create, :update, :destroy]
|
|
76
|
+
# end
|
|
77
|
+
#
|
|
78
|
+
# Takes the same options as before_action. Everything NOT listed keeps the
|
|
79
|
+
# cached check, which is the point: an app pays for a round trip on the few
|
|
80
|
+
# actions that cannot be undone, and nowhere else.
|
|
81
|
+
def clowk_require_fresh_session(**options)
|
|
82
|
+
before_action(**options) { clowk_enforce_fresh_session! }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
58
86
|
# Per-request credentials — for apps whose keys are not a boot constant:
|
|
59
87
|
# an operator pastes a publishable key into a settings screen, or one
|
|
60
88
|
# process serves several tenants.
|
|
@@ -90,27 +118,45 @@ module Clowk
|
|
|
90
118
|
clowk_current_resource.present?
|
|
91
119
|
end
|
|
92
120
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
121
|
+
# @param force [Boolean] ignore any cached status and ask Clowk now
|
|
122
|
+
def clowk_session_status(force: false)
|
|
123
|
+
return @clowk_session_status if defined?(@clowk_session_status) && !force
|
|
96
124
|
|
|
97
|
-
|
|
98
|
-
clowk_session_status&.dig(:status) == "active"
|
|
125
|
+
@clowk_session_status = resolve_session_status(force: force)
|
|
99
126
|
end
|
|
100
127
|
|
|
101
|
-
|
|
102
|
-
|
|
128
|
+
# @param force [Boolean] see {#clowk_session_status}
|
|
129
|
+
def clowk_session_active?(force: false)
|
|
130
|
+
status = clowk_session_status(force: force)
|
|
103
131
|
|
|
104
|
-
|
|
105
|
-
|
|
132
|
+
# "Could not ask" rather than "not active": a blip on the way to a single
|
|
133
|
+
# droplet must not sign everyone out. max_session_age is the bound on how
|
|
134
|
+
# long that can carry a session Clowk would have refused.
|
|
135
|
+
return true if @clowk_session_check_unavailable && Clowk.config.fail_open_on_broker_error
|
|
106
136
|
|
|
107
|
-
|
|
108
|
-
|
|
137
|
+
status&.dig(:status) == "active"
|
|
138
|
+
end
|
|
109
139
|
|
|
110
|
-
|
|
111
|
-
|
|
140
|
+
# Ends the session unless Clowk says, right now, that it still stands.
|
|
141
|
+
#
|
|
142
|
+
# For the handful of actions where a cached "active" is not good enough:
|
|
143
|
+
# rotating a secret, deleting an account, removing a member. Everything else
|
|
144
|
+
# should take the cached check — this is a round trip, on purpose.
|
|
145
|
+
#
|
|
146
|
+
# clowk_require_fresh_session only: [:destroy, :rotate_secret]
|
|
147
|
+
#
|
|
148
|
+
# Before 0.7 the only way to get this was `session_status_ttl = 0`, which
|
|
149
|
+
# bought freshness here by paying a round trip on every page instead.
|
|
150
|
+
def clowk_enforce_fresh_session!
|
|
151
|
+
clowk_enforce_session!(force: true)
|
|
152
|
+
end
|
|
112
153
|
|
|
113
|
-
|
|
154
|
+
# @param force [Boolean] see {#clowk_session_status}
|
|
155
|
+
def clowk_enforce_session!(force: false)
|
|
156
|
+
return clowk_expire_session!(nil) if clowk_session_beyond_max_age?
|
|
157
|
+
return if clowk_session_active?(force: force)
|
|
158
|
+
|
|
159
|
+
clowk_expire_session!(clowk_session_status)
|
|
114
160
|
end
|
|
115
161
|
|
|
116
162
|
def clowk_authenticate!
|
|
@@ -158,6 +204,37 @@ module Clowk
|
|
|
158
204
|
end
|
|
159
205
|
end
|
|
160
206
|
|
|
207
|
+
# One route out of a session that must end, whatever ended it — the broker
|
|
208
|
+
# said inactive, or the local ceiling passed. Apps hook it with
|
|
209
|
+
# config.on_session_expired; the default answers 401 or redirects.
|
|
210
|
+
def clowk_expire_session!(session_info)
|
|
211
|
+
callback = Clowk.config.on_session_expired
|
|
212
|
+
|
|
213
|
+
if callback.respond_to?(:call)
|
|
214
|
+
callback.call(self, session_info)
|
|
215
|
+
|
|
216
|
+
return
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
clowk_handle_expired_session(session_info)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# A ceiling Clowk plays no part in. Without it, failing open on an
|
|
223
|
+
# unreachable broker would mean a session that never ends.
|
|
224
|
+
#
|
|
225
|
+
# signed_in_at is stamped once, when the session is established:
|
|
226
|
+
# clowk_current_resource prefers the stored payload, so persist_clowk_session
|
|
227
|
+
# does not run again while the session stands.
|
|
228
|
+
def clowk_session_beyond_max_age?
|
|
229
|
+
max = Clowk.config.max_session_age.to_i
|
|
230
|
+
|
|
231
|
+
return false unless max.positive?
|
|
232
|
+
|
|
233
|
+
started = (stored_session&.dig("signed_in_at") || stored_session&.dig(:signed_in_at)).to_i
|
|
234
|
+
|
|
235
|
+
started.positive? && (Time.now.to_i - started) > max
|
|
236
|
+
end
|
|
237
|
+
|
|
161
238
|
def clowk_handle_expired_session(_session_info)
|
|
162
239
|
if clowk_api_request?
|
|
163
240
|
render json: {error: "Session expired or inactive"}, status: :unauthorized
|
|
@@ -253,8 +330,9 @@ module Clowk
|
|
|
253
330
|
})
|
|
254
331
|
end
|
|
255
332
|
|
|
256
|
-
def resolve_session_status
|
|
257
|
-
|
|
333
|
+
def resolve_session_status(force: false)
|
|
334
|
+
@clowk_session_check_unavailable = false
|
|
335
|
+
cached = force ? nil : clowk_read_cached_session_status
|
|
258
336
|
|
|
259
337
|
return cached if cached
|
|
260
338
|
|
|
@@ -273,6 +351,14 @@ module Clowk
|
|
|
273
351
|
|
|
274
352
|
status
|
|
275
353
|
rescue Clowk::InvalidTokenError
|
|
354
|
+
nil
|
|
355
|
+
rescue *BROKER_UNAVAILABLE => e
|
|
356
|
+
# Never cached: "we could not ask" is not an answer worth keeping, and the
|
|
357
|
+
# next request should try again rather than inherit this one's bad luck.
|
|
358
|
+
@clowk_session_check_unavailable = true
|
|
359
|
+
|
|
360
|
+
Clowk.config.http_logger&.warn("[Clowk] session check unavailable: #{e.class}: #{e.message}")
|
|
361
|
+
|
|
276
362
|
nil
|
|
277
363
|
end
|
|
278
364
|
|
data/lib/clowk/configuration.rb
CHANGED
|
@@ -25,6 +25,8 @@ module Clowk
|
|
|
25
25
|
attr_accessor :enforce_active_session
|
|
26
26
|
attr_accessor :on_session_expired
|
|
27
27
|
attr_accessor :session_status_ttl
|
|
28
|
+
attr_accessor :max_session_age
|
|
29
|
+
attr_accessor :fail_open_on_broker_error
|
|
28
30
|
attr_writer :session_status_cache
|
|
29
31
|
|
|
30
32
|
def initialize
|
|
@@ -52,6 +54,16 @@ module Clowk
|
|
|
52
54
|
# which silently turns every later enforcement call into a no-op. Set 0 to
|
|
53
55
|
# check on every call.
|
|
54
56
|
@session_status_ttl = 300
|
|
57
|
+
|
|
58
|
+
# A local ceiling the broker plays no part in, so a permanently
|
|
59
|
+
# unreachable Clowk cannot keep a session alive forever — the other half
|
|
60
|
+
# of failing open. nil leaves the broker as the only authority.
|
|
61
|
+
@max_session_age = nil
|
|
62
|
+
|
|
63
|
+
# A network blip must not sign everyone out. When the liveness check
|
|
64
|
+
# cannot be made at all, the session is left standing and checked again on
|
|
65
|
+
# the next request; max_session_age is what bounds that.
|
|
66
|
+
@fail_open_on_broker_error = true
|
|
55
67
|
end
|
|
56
68
|
|
|
57
69
|
# Where API-only apps cache session status, since they have no Rails session
|
data/lib/clowk/version.rb
CHANGED