devise-jwt-cookie2 0.6.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b102a530ecfb53046e5a429698e2db4c373e8f3a9569dc51d1153c12bbf10cae
4
- data.tar.gz: ac17f1d3252c92b4e087472555017d856c815f8a456808af1dfc7acae77a8099
3
+ metadata.gz: 580f1a06dbf4da8cca7f50a859e03bbbc7694068fe378ab35a0c7b936d148581
4
+ data.tar.gz: 50a30ab8297ca621423dc54e0eebb103fc9c4d766f1af7a8c73f9bbbd96d234b
5
5
  SHA512:
6
- metadata.gz: 8df2266a978c16560d9e307adbc83dc510ca5a1b1751ffb0ecc27d0d4daef394fce4a1d6dfe18a337cb6379ce72657efce51e0008a163e8b90df3e7443b311c4
7
- data.tar.gz: 4d91703d5c8b425b52dfe777491135b1ae9633c9251331786345b722516f3c4a15ca349dbc92b43bfaf5761c56bbbb4a41122af405c9ba180c7fbd2348f38996
6
+ metadata.gz: 3f40e189895d93d6dea89dea6b663a3b488ffa20af27d171cb6e2f83e8fbd3e76e05da844a142a0bf80426078022ae6dd8431413064538e6b1554aba31df67f8
7
+ data.tar.gz: a59ad9854285ba3380bec8b0a1c9337c8b3c17dab4f66554a3b0843c9a11e270ee3f5ac30adc5541d7d43daa981175a405947c68e9b5f5229156463d07c506c8
data/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  Add this line to your application's Gemfile:
8
8
 
9
9
  ```ruby
10
- gem 'devise-jwt-cookie2', '~> 0.6.0'
10
+ gem 'devise-jwt-cookie2', '~> 0.7.0'
11
11
  ```
12
12
 
13
13
  And then execute:
@@ -66,3 +66,17 @@ The domain the cookie should be issued to. Will be omitted if not set.
66
66
  #### same_site ([MDN](https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Cookies#samesite))
67
67
 
68
68
  Set's the SameSite attribute on the cookie. Defaults to `none`.
69
+
70
+ #### refresher
71
+
72
+ An object that renews the JWT while the cookie strategy authenticates a request. Defaults to `nil`, which never renews a token.
73
+
74
+ Set it to `Devise::JWT::Cookie::Refresher.new` to re-issue a still-valid token once it is past half its lifetime, so an active user's cookie slides forward instead of expiring:
75
+
76
+ ```ruby
77
+ config.jwt_cookie do |jwt_cookie|
78
+ jwt_cookie.refresher = Devise::JWT::Cookie::Refresher.new
79
+ end
80
+ ```
81
+
82
+ A refresher responds to `call(token, scope, env)`, where `token` is the cookie's JWT (`nil` when the request has none), and returns a new JWT or `nil` to keep the current one. The strategy authenticates the request with the returned token and the middleware writes it to the cookie, unless the request revokes the token. Subclass `Refresher` to supply your own rules, such as recovering a session from a separate refresh credential after the JWT has expired, or capping a session's total lifetime. The private `issue(user, scope, aud)` helper encodes a token and calls the model's `on_jwt_dispatch`, so revocation strategies such as `Allowlist` record it.
@@ -6,6 +6,7 @@ module Devise
6
6
  # Rack middleware to handle JWT token in cookies
7
7
  class Middleware
8
8
  ENV_KEY = 'warden-jwt_auth.token'
9
+ REFRESHED_TOKEN_KEY = 'devise-jwt-cookie.refreshed_token'
9
10
 
10
11
  attr_reader :app, :config
11
12
 
@@ -30,6 +31,9 @@ module Devise
30
31
  elsif token_should_be_revoked
31
32
  name, cookie = CookieHelper.new.build(nil)
32
33
  Rack::Utils.set_cookie_header!(headers, name, cookie)
34
+ elsif env[REFRESHED_TOKEN_KEY]
35
+ name, cookie = CookieHelper.new.build(env[REFRESHED_TOKEN_KEY])
36
+ Rack::Utils.set_cookie_header!(headers, name, cookie)
33
37
  end
34
38
  [status, headers, response]
35
39
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devise
4
+ module JWT
5
+ module Cookie
6
+ # Default refresher: re-issues a still-valid JWT once it is past half its lifetime, so a user who
7
+ # keeps making requests never sees the cookie expire. Subclass it to apply your own session
8
+ # rules, such as recovering a session after the JWT has expired or capping its total lifetime.
9
+ #
10
+ # A refresher receives the cookie's token (nil when there is none), the Warden scope and the
11
+ # Rack env, and returns a new token to authenticate the request with, or nil to keep the
12
+ # current one. The middleware writes a returned token to the cookie.
13
+ class Refresher
14
+ def call(token, scope, env)
15
+ return if token.nil?
16
+ return unless renew?(Warden::JWTAuth::TokenDecoder.new.call(token))
17
+
18
+ aud = Warden::JWTAuth::EnvHelper.aud_header(env)
19
+ issue(Warden::JWTAuth::UserDecoder.new.call(token, scope, aud), scope, aud)
20
+ rescue ::JWT::DecodeError
21
+ nil
22
+ end
23
+
24
+ private
25
+
26
+ def renew?(payload)
27
+ issued_at, expires_at = payload.values_at('iat', 'exp')
28
+ return false unless issued_at && expires_at
29
+
30
+ Time.now.to_i >= issued_at + ((expires_at - issued_at) / 2)
31
+ end
32
+
33
+ # Mirrors devise-jwt's dispatch hook, so revocation strategies such as Allowlist record the token.
34
+ def issue(user, scope, aud)
35
+ token, payload = Warden::JWTAuth::UserEncoder.new.call(user, scope, aud)
36
+ user.on_jwt_dispatch(token, payload) if user.respond_to?(:on_jwt_dispatch)
37
+ token
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -9,7 +9,7 @@ module Devise
9
9
  # an http-only cookie
10
10
  class Strategy < Warden::Strategies::Base
11
11
  def valid?
12
- !token.nil?
12
+ !token.nil? || !refresher.nil?
13
13
  end
14
14
 
15
15
  def store?
@@ -17,6 +17,10 @@ module Devise
17
17
  end
18
18
 
19
19
  def authenticate!
20
+ refresh
21
+ # Without a token there is nothing to reject, so other strategies still get a turn.
22
+ return if token.nil?
23
+
20
24
  aud = Warden::JWTAuth::EnvHelper.aud_header(env)
21
25
  user = Warden::JWTAuth::UserDecoder.new.call(token, scope, aud)
22
26
  success!(user)
@@ -29,6 +33,19 @@ module Devise
29
33
  def token
30
34
  @token ||= CookieHelper.new.read_from(cookies)
31
35
  end
36
+
37
+ def refresher
38
+ Cookie.config.refresher
39
+ end
40
+
41
+ # Warden re-runs a strategy that did not succeed or fail, so only ask the refresher once.
42
+ def refresh
43
+ return if @refreshed
44
+
45
+ @refreshed = true
46
+ refreshed_token = refresher&.call(token, scope, env)
47
+ env[Middleware::REFRESHED_TOKEN_KEY] = @token = refreshed_token if refreshed_token
48
+ end
32
49
  end
33
50
  end
34
51
  end
@@ -3,7 +3,7 @@
3
3
  module Devise
4
4
  module JWT
5
5
  module Cookie
6
- VERSION = '0.6.0'
6
+ VERSION = '0.7.0'
7
7
  end
8
8
  end
9
9
  end
@@ -24,6 +24,7 @@ module Devise
24
24
  setting :httponly, default: true
25
25
  setting :same_site, default: :none
26
26
  setting :domain
27
+ setting :refresher
27
28
 
28
29
  Import = Dry::AutoInject(config)
29
30
  end
@@ -34,4 +35,5 @@ require 'devise/jwt/cookie/version'
34
35
  require 'devise/jwt/cookie/railtie'
35
36
  require 'devise/jwt/cookie/cookie_helper'
36
37
  require 'devise/jwt/cookie/middleware'
38
+ require 'devise/jwt/cookie/refresher'
37
39
  require 'devise/jwt/cookie/models'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devise-jwt-cookie2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Major League Hacking
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-02-18 00:00:00.000000000 Z
11
+ date: 2026-10-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: devise-jwt
@@ -193,6 +193,7 @@ files:
193
193
  - lib/devise/jwt/cookie/models.rb
194
194
  - lib/devise/jwt/cookie/models/jwt_cookie_authenticatable.rb
195
195
  - lib/devise/jwt/cookie/railtie.rb
196
+ - lib/devise/jwt/cookie/refresher.rb
196
197
  - lib/devise/jwt/cookie/strategy.rb
197
198
  - lib/devise/jwt/cookie/version.rb
198
199
  - lib/devise/jwt/cookie2.rb