standard_id 0.43.1 → 0.43.2
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/CHANGELOG.md +18 -0
- data/app/jobs/standard_id/cleanup_expired_sessions_job.rb +58 -2
- data/lib/standard_id/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4bb59a29db3ed35482b44a1a0755a3827d7e02a6ad3a78bad5e5bd22e309d048
|
|
4
|
+
data.tar.gz: 93491ec90c91e2fdc6cf900bc62f9475ee786c723f4591dc40446964fbac1492
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0daceb3e839301ec3b294b8aa36af72441ec5b52ea82bcdcb7066fbc1bfef97cb5efaa11d62f729995c4a7ad22b22afd946e3ed1ff4999fa694a3dc2446b8a50
|
|
7
|
+
data.tar.gz: ab971a917f3ea552ca3c54d6928a88086bfef17066131cb727a12df721d6bfd2988fb348a33ded3fd529d09102ea053b904ff5146129e9b5008e10e64d7b2f15
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.43.2] - 2026-09-25
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`CleanupExpiredSessionsJob` failed on every run once an expired session had a refresh token** (sidekick-web SIDEKICK-WEB-3K / cron monitor SIDEKICK-WEB-3M: `PG::ForeignKeyViolation … violates foreign key constraint "fk_rails_db44ba6f6e" on table "standard_id_refresh_tokens"`). `standard_id_refresh_tokens.session_id` references `standard_id_sessions` with no `ON DELETE` action, and `Session.where(...).delete_all` skipped the model's `dependent: :nullify`. This affected every host that schedules the job, directly or through `CleanupAllJob`, and nothing was ever cleaned up. Now, per batch of 1,000 (`batch_size:`), in one transaction, with the candidate rows re-checked under `FOR UPDATE SKIP LOCKED`:
|
|
15
|
+
- **An expired session that still has a live refresh token (unrevoked, unexpired) is kept.** Refresh tokens deliberately outlive their session's expiry: `RefreshTokenFlow#validate_parent_session!` checks revocation, not expiry, since 0.35.x, so `refresh_token_lifetime` alone decides how long a client stays signed in (jumpdrive-web runs 1-day browser sessions against 30-day refresh tokens). Deleting such a session would detach its token and lose "revoking this session ends its access". Revoking the token, as `Session#destroy` does, would sign the client out early. The session is collected on a later run, once its tokens are dead, so at most `refresh_token_lifetime` after it would otherwise have gone.
|
|
16
|
+
- **Dead refresh tokens (revoked or expired) of the sessions being deleted are detached** (`session_id` → `NULL`, as `dependent: :nullify` does) rather than deleted. `CleanupExpiredRefreshTokensJob` removes them on its own window, and until then a replayed revoked token still triggers reuse detection.
|
|
17
|
+
- A host whose copy of the migration added `on_delete: :nullify` to that FK (sidekick-web's `db/schema.rb` shows one, although its production constraint evidently has none) no longer has live tokens silently detached from their sessions either.
|
|
18
|
+
- The other cleanup jobs had no such hazard. Nothing references `standard_id_authorization_codes` or `standard_id_code_challenges`, and the only foreign key into `standard_id_refresh_tokens` is its own `previous_token_id`, which is `ON DELETE SET NULL`. `standard_id_refresh_tokens.session_id` is the only foreign key into `standard_id_sessions`, in the gem, standard_id-provider and all five apps.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **Requires Rails 8.1** (`rails >= 8.1`, was `>= 8.0`; #344, merged since 0.43.1). All five consumer apps already run Rails 8.1.3.1, and 8.0 was never tested in CI.
|
|
23
|
+
|
|
24
|
+
### Upgrade notes
|
|
25
|
+
|
|
26
|
+
Bump only. The first run after upgrading deletes, in batches, the backlog of expired sessions the job never managed to remove.
|
|
27
|
+
|
|
10
28
|
## [0.43.1] - 2026-09-24
|
|
11
29
|
|
|
12
30
|
Fixes found while shipping 0.43.0. **Take this instead of 0.43.0** — hosts on `c.passwordless.delivery = :built_in` with the WebEngine mounted (jumpdrive-web, nutripod-web) would otherwise email email-verification codes as "your sign-in code".
|
|
@@ -2,14 +2,70 @@ module StandardId
|
|
|
2
2
|
class CleanupExpiredSessionsJob < ApplicationJob
|
|
3
3
|
queue_as :default
|
|
4
4
|
|
|
5
|
+
DEFAULT_BATCH_SIZE = 1_000
|
|
6
|
+
|
|
5
7
|
# Delete sessions that expired more than `grace_period_seconds` ago.
|
|
6
8
|
# A grace period avoids deleting sessions that just expired and might
|
|
7
9
|
# still be referenced in in-flight requests.
|
|
8
10
|
# Accepts integer seconds for reliable ActiveJob serialization across all queue adapters.
|
|
9
|
-
|
|
11
|
+
#
|
|
12
|
+
# Refresh tokens reference their session (standard_id_refresh_tokens.session_id,
|
|
13
|
+
# a foreign key with no ON DELETE action in the gem's migration), and
|
|
14
|
+
# `delete_all` skips the model's `dependent: :nullify`, so a bare delete
|
|
15
|
+
# failed the whole run once any expired session had a refresh token
|
|
16
|
+
# (sidekick-web SIDEKICK-WEB-3K). Per batch, in one transaction:
|
|
17
|
+
#
|
|
18
|
+
# * An expired session that still has a LIVE refresh token (unrevoked,
|
|
19
|
+
# unexpired) is kept. Refresh tokens deliberately outlive their session's
|
|
20
|
+
# expiry — RefreshTokenFlow#validate_parent_session! checks revocation,
|
|
21
|
+
# not expiry, so refresh_token_lifetime alone governs how long a client
|
|
22
|
+
# stays signed in. Deleting the session would detach the token from it and
|
|
23
|
+
# lose "revoking this session ends its access"; revoking the token (what
|
|
24
|
+
# Session#destroy does) would sign the client out early. The session is
|
|
25
|
+
# collected on a later run, once its tokens are dead — at most
|
|
26
|
+
# refresh_token_lifetime later.
|
|
27
|
+
# * Dead refresh tokens (revoked or expired) of the sessions being deleted
|
|
28
|
+
# are detached (`session_id` → NULL, the model's `dependent: :nullify`),
|
|
29
|
+
# not deleted: CleanupExpiredRefreshTokensJob removes them on its own
|
|
30
|
+
# window, and until then a replayed revoked token still triggers reuse
|
|
31
|
+
# detection.
|
|
32
|
+
#
|
|
33
|
+
# Batches (`in_batches`) keep each transaction and its locks small.
|
|
34
|
+
def perform(grace_period_seconds: 7.days.to_i, batch_size: DEFAULT_BATCH_SIZE)
|
|
10
35
|
cutoff = grace_period_seconds.seconds.ago
|
|
11
|
-
deleted =
|
|
36
|
+
deleted = 0
|
|
37
|
+
|
|
38
|
+
StandardId::Session.where("expires_at < ?", cutoff).in_batches(of: batch_size) do |batch|
|
|
39
|
+
deleted += delete_batch(batch.pluck(:id), cutoff)
|
|
40
|
+
end
|
|
41
|
+
|
|
12
42
|
Rails.logger.info("[StandardId] Cleaned up #{deleted} expired sessions older than #{cutoff}")
|
|
13
43
|
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def delete_batch(candidate_ids, cutoff)
|
|
48
|
+
StandardId::Session.transaction do
|
|
49
|
+
# Re-check under a row lock: a sign-in may have revived one of these
|
|
50
|
+
# rows (OauthSessionPersistence reuses an expired, unrevoked device
|
|
51
|
+
# session and bumps expires_at) or issued it a refresh token since the
|
|
52
|
+
# batch was read. SKIP LOCKED leaves a row another transaction holds for
|
|
53
|
+
# the next run. (SQLite ignores the lock clause; it serialises writers.)
|
|
54
|
+
ids = StandardId::Session
|
|
55
|
+
.where(id: candidate_ids)
|
|
56
|
+
.where("expires_at < ?", cutoff)
|
|
57
|
+
.where.not(id: live_refresh_tokens.select(:session_id))
|
|
58
|
+
.lock("FOR UPDATE SKIP LOCKED")
|
|
59
|
+
.pluck(:id)
|
|
60
|
+
next 0 if ids.empty?
|
|
61
|
+
|
|
62
|
+
StandardId::RefreshToken.where(session_id: ids).update_all(session_id: nil)
|
|
63
|
+
StandardId::Session.where(id: ids).delete_all
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def live_refresh_tokens
|
|
68
|
+
StandardId::RefreshToken.active.where.not(session_id: nil)
|
|
69
|
+
end
|
|
14
70
|
end
|
|
15
71
|
end
|
data/lib/standard_id/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: standard_id
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.43.
|
|
4
|
+
version: 0.43.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jaryl Sim
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '8.
|
|
18
|
+
version: '8.1'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '8.
|
|
25
|
+
version: '8.1'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: bcrypt
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -345,7 +345,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
345
345
|
- !ruby/object:Gem::Version
|
|
346
346
|
version: '0'
|
|
347
347
|
requirements: []
|
|
348
|
-
rubygems_version: 4.0.
|
|
348
|
+
rubygems_version: 4.0.10
|
|
349
349
|
specification_version: 4
|
|
350
350
|
summary: A comprehensive authentication engine for Rails, built on the security primitives
|
|
351
351
|
introduced in Rails 8.
|