clowk 0.10.0 → 0.11.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 +41 -0
- data/lib/clowk/authenticable.rb +148 -11
- data/lib/clowk/configuration.rb +18 -0
- data/lib/clowk/controllers/callbacks_controller.rb +3 -0
- data/lib/clowk/sdk/session_token.rb +54 -0
- data/lib/clowk/token_refresher.rb +127 -0
- data/lib/clowk/version.rb +1 -1
- data/lib/clowk.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b724ef711942092b90d87857fea2d8d439f759813d9312cb8e20d30dcd7201e
|
|
4
|
+
data.tar.gz: 7c79da7ff63485cd70cc62fac3669a1e0f96041e52ac6782c8c229ffee28972a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c26df4690279d52da9f035e0636991a135d731b8472ce9071d65003507eb0272a80e7f909249f0e159c313f44fd6a4970d5568ea635517b9278abe1cfda84480
|
|
7
|
+
data.tar.gz: e8a253974a0b9dcb0610a47b3e0e26b9c24aca10799291b9abaa059399fa8b11a8ff68c1f870d11bbe6d97e715d61019994b2648a9d7b0a8fc5de690e2c36ed5
|
data/README.md
CHANGED
|
@@ -299,6 +299,47 @@ request, so an app does not have to wait for everyone to sign out.
|
|
|
299
299
|
Ask for `:app` if the app reads the token out of `session[...]` itself rather
|
|
300
300
|
than through `current_token`.
|
|
301
301
|
|
|
302
|
+
### How long a session lasts
|
|
303
|
+
|
|
304
|
+
The Clowk instance decides. Set the session lifetime in the dashboard (7 days,
|
|
305
|
+
30 days) and the app keeps the user signed in for exactly that long, also in an
|
|
306
|
+
installed PWA that the OS stops and starts again.
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
config.publishable_key = ENV["CLOWK_PUBLISHABLE_KEY"] # required for refresh tokens
|
|
310
|
+
config.refresh_tokens = true # default
|
|
311
|
+
config.refresh_cookie_key = "clowk_refresh_token" # default
|
|
312
|
+
config.refresh_skew = 60 # seconds, default
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
1. The callback trades the sign-in token for an access token and a refresh
|
|
316
|
+
token (`POST /sessions/exchange`).
|
|
317
|
+
2. Both go into httponly cookies that expire when the Clowk session ends. A
|
|
318
|
+
cookie with no expiry is deleted when the browser closes, and a PWA closes
|
|
319
|
+
each time the OS stops it. That was why a PWA lost its session in hours.
|
|
320
|
+
3. When the access token expires (or the app session cookie is gone), the next
|
|
321
|
+
request renews it with the refresh cookie. There is no redirect.
|
|
322
|
+
4. When Clowk refuses the refresh (revoked, past the lifetime, idle timeout),
|
|
323
|
+
the local session ends and the user goes to sign-in. When Clowk cannot be
|
|
324
|
+
reached, the cookies stay and the next request tries again.
|
|
325
|
+
5. `clowk_sign_out!` revokes the session on Clowk too.
|
|
326
|
+
|
|
327
|
+
Refresh tokens rotate: each one works once. A page and its Turbo frames can
|
|
328
|
+
arrive together with the same expired token, so one request rotates and the
|
|
329
|
+
others take its result from `config.session_status_cache` (`Rails.cache` by
|
|
330
|
+
default). Use a cache all app processes share (Solid Cache, Redis, Memcached).
|
|
331
|
+
|
|
332
|
+
Also give the app's own session cookie an expiry, so the app does not need a
|
|
333
|
+
refresh each time the PWA opens:
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
# config/initializers/session_store.rb
|
|
337
|
+
Rails.application.config.session_store :cookie_store, key: "_myapp_session", expire_after: 30.days
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Without a publishable key, or with `config.refresh_tokens = false`, the gem keeps
|
|
341
|
+
only the sign-in token, in a cookie that lives as long as the token.
|
|
342
|
+
|
|
302
343
|
### Token verification
|
|
303
344
|
|
|
304
345
|
Tokens signed with `RS256` are verified against Clowk's public key set, fetched
|
data/lib/clowk/authenticable.rb
CHANGED
|
@@ -12,6 +12,10 @@ module Clowk
|
|
|
12
12
|
# must not be swallowed into "the session is probably fine".
|
|
13
13
|
BROKER_UNAVAILABLE = [SystemCallError, Timeout::Error, IOError, SocketError, EOFError].freeze
|
|
14
14
|
|
|
15
|
+
# How long the cookies live when Clowk does not say (a server older than
|
|
16
|
+
# refresh_expires_in). Clowk's own default session lifetime.
|
|
17
|
+
FALLBACK_SESSION_LIFETIME = 7 * 24 * 60 * 60
|
|
18
|
+
|
|
15
19
|
def self.install_dynamic_methods(base)
|
|
16
20
|
scope = Clowk.config.prefix_by.to_s
|
|
17
21
|
current_method = :"current_#{scope}"
|
|
@@ -89,6 +93,8 @@ module Clowk
|
|
|
89
93
|
|
|
90
94
|
def clowk_current_resource
|
|
91
95
|
@clowk_current_resource ||= begin
|
|
96
|
+
clowk_end_lapsed_session!
|
|
97
|
+
|
|
92
98
|
payload = stored_user_payload || verified_request_payload
|
|
93
99
|
|
|
94
100
|
drop_mirrored_token!
|
|
@@ -101,8 +107,13 @@ module Clowk
|
|
|
101
107
|
# and, for one request after an app drops that setting, in a session
|
|
102
108
|
# written before the change. drop_mirrored_token! clears those. Clowk's own
|
|
103
109
|
# cookie is the source either way.
|
|
110
|
+
#
|
|
111
|
+
# An expired token is renewed with the refresh cookie before it is handed
|
|
112
|
+
# out, so an API call or a session check never carries a dead token.
|
|
104
113
|
def current_token
|
|
105
|
-
stored_session&.dig("token") || extracted_token
|
|
114
|
+
token = stored_session&.dig("token") || extracted_token
|
|
115
|
+
|
|
116
|
+
clowk_refresh_due?(token) ? clowk_fresh_token : token
|
|
106
117
|
end
|
|
107
118
|
|
|
108
119
|
def clowk_signed_in?
|
|
@@ -175,9 +186,14 @@ module Clowk
|
|
|
175
186
|
clowk_current_resource
|
|
176
187
|
end
|
|
177
188
|
|
|
178
|
-
|
|
189
|
+
# @param revoke [Boolean] also end the session on Clowk, so the refresh
|
|
190
|
+
# token cannot sign anyone in again
|
|
191
|
+
def clowk_sign_out!(revoke: true)
|
|
192
|
+
Clowk::TokenRefresher.new.revoke(clowk_refresh_cookie) if revoke
|
|
193
|
+
|
|
179
194
|
clowk_session_store&.delete(Clowk.config.session_key)
|
|
180
195
|
clowk_cookie_jar&.delete(Clowk.config.cookie_key)
|
|
196
|
+
clowk_cookie_jar&.delete(Clowk.config.refresh_cookie_key)
|
|
181
197
|
|
|
182
198
|
@clowk_current_resource = nil
|
|
183
199
|
end
|
|
@@ -277,16 +293,132 @@ module Clowk
|
|
|
277
293
|
end
|
|
278
294
|
|
|
279
295
|
def verified_request_payload
|
|
280
|
-
|
|
296
|
+
token = clowk_refresh_due?(extracted_token) ? clowk_fresh_token : extracted_token
|
|
297
|
+
|
|
298
|
+
return unless token
|
|
281
299
|
|
|
282
|
-
payload = Clowk::JwtVerifier.new.verify(
|
|
283
|
-
|
|
300
|
+
payload = Clowk::JwtVerifier.new.verify(token)
|
|
301
|
+
|
|
302
|
+
persist_clowk_session(token, payload)
|
|
284
303
|
|
|
285
304
|
payload
|
|
286
305
|
rescue Clowk::InvalidTokenError
|
|
287
306
|
nil
|
|
288
307
|
end
|
|
289
308
|
|
|
309
|
+
# Trades the sign-in token for an access + refresh pair, so the session
|
|
310
|
+
# lasts what the Clowk instance configures. Falls back to the sign-in
|
|
311
|
+
# token alone when there is no publishable key, the token has no session,
|
|
312
|
+
# or Clowk cannot be reached.
|
|
313
|
+
def clowk_exchange_sign_in_token(token)
|
|
314
|
+
return token unless Clowk.config.refresh_tokens && clowk_cookie_jar
|
|
315
|
+
|
|
316
|
+
pair = Clowk::TokenRefresher.new.exchange(token)
|
|
317
|
+
|
|
318
|
+
return token unless pair
|
|
319
|
+
|
|
320
|
+
clowk_adopt_token_pair!(pair)
|
|
321
|
+
|
|
322
|
+
pair[:access_token]
|
|
323
|
+
rescue Clowk::InvalidTokenError, *BROKER_UNAVAILABLE => e
|
|
324
|
+
Clowk.config.http_logger&.warn("[Clowk] token exchange skipped: #{e.class}: #{e.message}")
|
|
325
|
+
|
|
326
|
+
token
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def clowk_refresh_due?(token)
|
|
330
|
+
return false unless Clowk.config.refresh_tokens
|
|
331
|
+
return false if clowk_refresh_cookie.blank?
|
|
332
|
+
return true if token.blank?
|
|
333
|
+
|
|
334
|
+
expires_at = clowk_token_expiry(token)
|
|
335
|
+
|
|
336
|
+
expires_at.nil? || expires_at - Clowk.config.refresh_skew.to_i <= Time.now.to_i
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# A refused refresh means the session is over on Clowk (revoked, past its
|
|
340
|
+
# lifetime, idle), so the local one ends too. An unreachable Clowk is not
|
|
341
|
+
# an answer: the cookies stay for the next request.
|
|
342
|
+
def clowk_fresh_token
|
|
343
|
+
return @clowk_fresh_token if defined?(@clowk_fresh_token)
|
|
344
|
+
|
|
345
|
+
@clowk_fresh_token = begin
|
|
346
|
+
pair = Clowk::TokenRefresher.new.refresh(clowk_refresh_cookie)
|
|
347
|
+
|
|
348
|
+
clowk_adopt_token_pair!(pair)
|
|
349
|
+
|
|
350
|
+
pair[:access_token]
|
|
351
|
+
rescue Clowk::InvalidTokenError
|
|
352
|
+
clowk_sign_out!(revoke: false)
|
|
353
|
+
|
|
354
|
+
nil
|
|
355
|
+
rescue *BROKER_UNAVAILABLE => e
|
|
356
|
+
Clowk.config.http_logger&.warn("[Clowk] token refresh unavailable: #{e.class}: #{e.message}")
|
|
357
|
+
|
|
358
|
+
extracted_token
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Both cookies end when the Clowk session ends. A cookie with no expiry is
|
|
363
|
+
# deleted when the browser closes, and an installed PWA closes each time
|
|
364
|
+
# the OS stops it.
|
|
365
|
+
def clowk_adopt_token_pair!(pair)
|
|
366
|
+
expires_at = Time.now + (pair[:refresh_expires_in].presence || FALLBACK_SESSION_LIFETIME).to_i
|
|
367
|
+
|
|
368
|
+
@clowk_session_expires_at = expires_at
|
|
369
|
+
|
|
370
|
+
clowk_write_cookie(Clowk.config.cookie_key, pair[:access_token], expires_at)
|
|
371
|
+
clowk_write_cookie(Clowk.config.refresh_cookie_key, pair[:refresh_token], expires_at) if pair[:refresh_token].present?
|
|
372
|
+
|
|
373
|
+
held = stored_session
|
|
374
|
+
store = clowk_session_store
|
|
375
|
+
|
|
376
|
+
return if held.nil? || store.nil?
|
|
377
|
+
|
|
378
|
+
updated = held.merge("expires_at" => expires_at.to_i)
|
|
379
|
+
updated["token"] = pair[:access_token] if Clowk.config.token_store == :app
|
|
380
|
+
|
|
381
|
+
store[Clowk.config.session_key] = updated
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def clowk_write_cookie(key, value, expires_at)
|
|
385
|
+
clowk_cookie_jar&.[]=(key, {
|
|
386
|
+
value: value,
|
|
387
|
+
expires: expires_at,
|
|
388
|
+
httponly: true,
|
|
389
|
+
same_site: :lax,
|
|
390
|
+
secure: clowk_secure_request?
|
|
391
|
+
})
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def clowk_secure_request?
|
|
395
|
+
request.respond_to?(:ssl?) && request.ssl?
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def clowk_refresh_cookie
|
|
399
|
+
clowk_cookie_jar&.[](Clowk.config.refresh_cookie_key).presence
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def clowk_token_expiry(token)
|
|
403
|
+
JWT.decode(token, nil, false).first["exp"]&.to_i
|
|
404
|
+
rescue JWT::DecodeError
|
|
405
|
+
nil
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# The stored session carries the end of the Clowk session. Past it, the
|
|
409
|
+
# local session ends too, even when the host app's session cookie lives on.
|
|
410
|
+
def clowk_end_lapsed_session!
|
|
411
|
+
held = stored_session
|
|
412
|
+
|
|
413
|
+
return if held.nil?
|
|
414
|
+
|
|
415
|
+
expires_at = (held["expires_at"] || held[:expires_at]).to_i
|
|
416
|
+
|
|
417
|
+
return unless expires_at.positive? && expires_at <= Time.now.to_i
|
|
418
|
+
|
|
419
|
+
clowk_sign_out!(revoke: false)
|
|
420
|
+
end
|
|
421
|
+
|
|
290
422
|
# Bearer header or cookie only — never the query string. A valid token in a
|
|
291
423
|
# URL would otherwise sign the visitor in on ANY path, bypassing the state
|
|
292
424
|
# check that makes the OAuth callback safe, and would stay replayable for the
|
|
@@ -319,16 +451,21 @@ module Clowk
|
|
|
319
451
|
return if store.nil?
|
|
320
452
|
|
|
321
453
|
claims = {user: payload, signed_in_at: Time.now.to_i}
|
|
454
|
+
claims[:expires_at] = @clowk_session_expires_at.to_i if @clowk_session_expires_at
|
|
322
455
|
|
|
323
456
|
store[Clowk.config.session_key] =
|
|
324
457
|
(Clowk.config.token_store == :app) ? claims.merge(token:) : claims
|
|
325
458
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
459
|
+
return if @clowk_session_expires_at
|
|
460
|
+
|
|
461
|
+
clowk_write_cookie(Clowk.config.cookie_key, token, clowk_token_cookie_expiry(token))
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# Without a refresh token the cookie lives as long as the token does.
|
|
465
|
+
def clowk_token_cookie_expiry(token)
|
|
466
|
+
expires_at = clowk_token_expiry(token)
|
|
467
|
+
|
|
468
|
+
expires_at ? Time.at(expires_at) : nil
|
|
332
469
|
end
|
|
333
470
|
|
|
334
471
|
# A session written under :app still carries the token, and
|
data/lib/clowk/configuration.rb
CHANGED
|
@@ -28,6 +28,9 @@ module Clowk
|
|
|
28
28
|
attr_accessor :max_session_age
|
|
29
29
|
attr_accessor :fail_open_on_broker_error
|
|
30
30
|
attr_accessor :token_store
|
|
31
|
+
attr_accessor :refresh_tokens
|
|
32
|
+
attr_accessor :refresh_cookie_key
|
|
33
|
+
attr_accessor :refresh_skew
|
|
31
34
|
attr_writer :session_status_cache
|
|
32
35
|
|
|
33
36
|
def initialize
|
|
@@ -86,6 +89,21 @@ module Clowk
|
|
|
86
89
|
# :app is there for an app that reads the token out of `session[...]`
|
|
87
90
|
# itself, rather than through `current_token`.
|
|
88
91
|
@token_store = nil
|
|
92
|
+
|
|
93
|
+
# Keeps the user signed in for the session lifetime the Clowk instance
|
|
94
|
+
# configures (7 days, 30 days, whatever the dashboard says), not for the
|
|
95
|
+
# one hour an access token lives.
|
|
96
|
+
#
|
|
97
|
+
# On sign-in the callback trades the token for an access + refresh pair.
|
|
98
|
+
# Both go into cookies that expire when the Clowk session ends, so they
|
|
99
|
+
# survive a closed browser or a stopped PWA. An expired access token is
|
|
100
|
+
# renewed on the next request, without a redirect. Needs the publishable
|
|
101
|
+
# key; without one the gem keeps only the sign-in token, as before.
|
|
102
|
+
@refresh_tokens = true
|
|
103
|
+
@refresh_cookie_key = "clowk_refresh_token"
|
|
104
|
+
|
|
105
|
+
# Renew the access token this many seconds before it expires.
|
|
106
|
+
@refresh_skew = 60
|
|
89
107
|
end
|
|
90
108
|
|
|
91
109
|
# Where API-only apps cache session status, since they have no Rails session
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clowk
|
|
4
|
+
module SDK
|
|
5
|
+
# The refresh token lifecycle. Authenticated with the publishable key: the
|
|
6
|
+
# refresh token is the credential.
|
|
7
|
+
#
|
|
8
|
+
# pair = client.session_tokens.exchange(token: sign_in_token)
|
|
9
|
+
# pair = client.session_tokens.refresh(refresh_token: pair[:refresh_token])
|
|
10
|
+
#
|
|
11
|
+
# Each pair is a Hash with :access_token, :refresh_token, :expires_in,
|
|
12
|
+
# :refresh_expires_in and :session_id. A refused call raises
|
|
13
|
+
# Clowk::InvalidTokenError.
|
|
14
|
+
class SessionToken < Resource
|
|
15
|
+
def self.resource_path
|
|
16
|
+
"sessions"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Trades the sign-in token from the callback for an access + refresh pair.
|
|
20
|
+
def exchange(token:)
|
|
21
|
+
pair_from(client.post("#{self.class.resource_path}/exchange", {token: token}))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Spends the refresh token and returns its successor.
|
|
25
|
+
def refresh(refresh_token:)
|
|
26
|
+
pair_from(client.post("#{self.class.resource_path}/refresh", {refresh_token: refresh_token}))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Ends the session the refresh token belongs to.
|
|
30
|
+
def revoke(refresh_token:)
|
|
31
|
+
client.post("#{self.class.resource_path}/revoke", {refresh_token: refresh_token})
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def pair_from(response)
|
|
37
|
+
body = response.body_parsed
|
|
38
|
+
|
|
39
|
+
unless response.success?
|
|
40
|
+
message = body.is_a?(Hash) ? body["error"] : nil
|
|
41
|
+
|
|
42
|
+
raise Clowk::InvalidTokenError, message || "session token request failed"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
data = (body.is_a?(Hash) && body["data"].is_a?(Hash)) ? body["data"] : body
|
|
46
|
+
pair = data.to_h.deep_symbolize_keys
|
|
47
|
+
|
|
48
|
+
raise Clowk::InvalidTokenError, "session token response has no access_token" if pair[:access_token].blank?
|
|
49
|
+
|
|
50
|
+
pair
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Clowk
|
|
6
|
+
# Exchanges and refreshes session tokens, one refresh per refresh token.
|
|
7
|
+
#
|
|
8
|
+
# Refresh tokens rotate: each one works once. A page and its Turbo frames
|
|
9
|
+
# can arrive together with the same expired access token, and each would
|
|
10
|
+
# spend the same refresh token. Only the first wins, and the rest would sign
|
|
11
|
+
# the user out. The first request takes a lock in the cache and shares the
|
|
12
|
+
# new pair; the others wait for it and use the same pair.
|
|
13
|
+
#
|
|
14
|
+
# The lock works across processes only with a shared cache (Solid Cache,
|
|
15
|
+
# Redis, Memcached). With a per-process cache, Clowk's grace window for a
|
|
16
|
+
# just-rotated token keeps the session alive, and the late request is
|
|
17
|
+
# signed in again on the next request.
|
|
18
|
+
class TokenRefresher
|
|
19
|
+
LOCK_TTL = 10
|
|
20
|
+
RESULT_TTL = 30
|
|
21
|
+
WAIT_TIMEOUT = 5.0
|
|
22
|
+
WAIT_INTERVAL = 0.05
|
|
23
|
+
|
|
24
|
+
def initialize(cache: Clowk.config.session_status_cache, client: nil)
|
|
25
|
+
@cache = cache
|
|
26
|
+
@client = client
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Hash, nil] the pair, or nil when no publishable key is set
|
|
30
|
+
# @raise [Clowk::InvalidTokenError] when Clowk refuses the token
|
|
31
|
+
def exchange(token)
|
|
32
|
+
return unless publishable_key?
|
|
33
|
+
|
|
34
|
+
sdk.session_tokens.exchange(token: token)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Hash] the new pair
|
|
38
|
+
# @raise [Clowk::InvalidTokenError] when Clowk refuses the refresh token
|
|
39
|
+
def refresh(refresh_token)
|
|
40
|
+
key = Digest::SHA256.hexdigest(refresh_token.to_s)
|
|
41
|
+
|
|
42
|
+
shared = read_shared(key)
|
|
43
|
+
|
|
44
|
+
return shared if shared
|
|
45
|
+
|
|
46
|
+
return rotate(refresh_token, key) if acquire_lock(key)
|
|
47
|
+
|
|
48
|
+
wait_for_shared(key) || raise(Clowk::InvalidTokenError, "concurrent refresh did not finish")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Ends the session on Clowk. Best effort: a sign-out must not fail because
|
|
52
|
+
# the broker is down.
|
|
53
|
+
def revoke(refresh_token)
|
|
54
|
+
return if refresh_token.blank? || !publishable_key?
|
|
55
|
+
|
|
56
|
+
sdk.session_tokens.revoke(refresh_token: refresh_token)
|
|
57
|
+
rescue Clowk::Error, *Clowk::Authenticable::BROKER_UNAVAILABLE
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
attr_reader :cache
|
|
64
|
+
|
|
65
|
+
def rotate(refresh_token, key)
|
|
66
|
+
pair = sdk.session_tokens.refresh(refresh_token: refresh_token)
|
|
67
|
+
|
|
68
|
+
cache&.write(result_key(key), pair, expires_in: RESULT_TTL)
|
|
69
|
+
|
|
70
|
+
pair
|
|
71
|
+
rescue Clowk::InvalidTokenError
|
|
72
|
+
cache&.write(result_key(key), {refused: true}, expires_in: RESULT_TTL)
|
|
73
|
+
|
|
74
|
+
raise
|
|
75
|
+
ensure
|
|
76
|
+
cache&.delete(lock_key(key))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def acquire_lock(key)
|
|
80
|
+
return true if cache.nil?
|
|
81
|
+
|
|
82
|
+
cache.write(lock_key(key), 1, unless_exist: true, expires_in: LOCK_TTL)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def wait_for_shared(key)
|
|
86
|
+
deadline = monotonic_now + WAIT_TIMEOUT
|
|
87
|
+
|
|
88
|
+
while monotonic_now < deadline
|
|
89
|
+
sleep(WAIT_INTERVAL)
|
|
90
|
+
|
|
91
|
+
shared = read_shared(key)
|
|
92
|
+
|
|
93
|
+
return shared if shared
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def read_shared(key)
|
|
100
|
+
shared = cache&.read(result_key(key))&.deep_symbolize_keys
|
|
101
|
+
|
|
102
|
+
raise Clowk::InvalidTokenError, "refresh token was refused" if shared&.dig(:refused)
|
|
103
|
+
|
|
104
|
+
shared
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def result_key(key)
|
|
108
|
+
"clowk:refresh:result:#{key}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def lock_key(key)
|
|
112
|
+
"clowk:refresh:lock:#{key}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def monotonic_now
|
|
116
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def publishable_key?
|
|
120
|
+
Clowk.credentials.publishable_key.present?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def sdk
|
|
124
|
+
@client ||= Clowk::SDK::Client.new
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
data/lib/clowk/version.rb
CHANGED
data/lib/clowk.rb
CHANGED
|
@@ -51,6 +51,7 @@ require_relative "clowk/sdk/session"
|
|
|
51
51
|
require_relative "clowk/sdk/subdomain"
|
|
52
52
|
require_relative "clowk/sdk/token"
|
|
53
53
|
require_relative "clowk/sdk/session_config"
|
|
54
|
+
require_relative "clowk/sdk/session_token"
|
|
54
55
|
require_relative "clowk/sdk/client"
|
|
55
56
|
require_relative "clowk/subdomain"
|
|
56
57
|
require_relative "clowk/jwks"
|
|
@@ -58,6 +59,7 @@ require_relative "clowk/jwt_verifier"
|
|
|
58
59
|
require_relative "clowk/helpers/url_helpers"
|
|
59
60
|
require_relative "clowk/middleware/token_extractor"
|
|
60
61
|
require_relative "clowk/authenticable"
|
|
62
|
+
require_relative "clowk/token_refresher"
|
|
61
63
|
require_relative "clowk/controllers/base_controller"
|
|
62
64
|
require_relative "clowk/controllers/callbacks_controller"
|
|
63
65
|
require_relative "clowk/controllers/sessions_controller"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: clowk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Clowk
|
|
@@ -123,10 +123,12 @@ files:
|
|
|
123
123
|
- lib/clowk/sdk/resource.rb
|
|
124
124
|
- lib/clowk/sdk/session.rb
|
|
125
125
|
- lib/clowk/sdk/session_config.rb
|
|
126
|
+
- lib/clowk/sdk/session_token.rb
|
|
126
127
|
- lib/clowk/sdk/subdomain.rb
|
|
127
128
|
- lib/clowk/sdk/token.rb
|
|
128
129
|
- lib/clowk/sdk/user.rb
|
|
129
130
|
- lib/clowk/subdomain.rb
|
|
131
|
+
- lib/clowk/token_refresher.rb
|
|
130
132
|
- lib/clowk/version.rb
|
|
131
133
|
homepage: https://clowk.in
|
|
132
134
|
licenses:
|