standard_id 0.36.1 → 0.37.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/CHANGELOG.md +46 -0
- data/README.md +14 -2
- data/app/controllers/standard_id/api/oauth/base_controller.rb +61 -0
- data/lib/standard_id/oauth/refresh_token_flow.rb +19 -2
- data/lib/standard_id/oauth/token_grant_flow.rb +34 -2
- data/lib/standard_id/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: 26e62c374edcaa53ab63756178e869a2d0c54c7ab00d26c2da823b28867fbeb6
|
|
4
|
+
data.tar.gz: 1a9a4e63f3e7f0e603c771c78f9fe503aad2768ea6302f675c360056f9d7c8d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d29d16c5b0d02017f574df54ee5a14a2455e4440da51c72d6cb480e89c284de9167fbb5f0dbdf4bb8e659009c2020ec06d80f7e7b56c2e06323969b5356c9db2
|
|
7
|
+
data.tar.gz: 510aaa0c2252ae3a1a1ab548a9f4272fdaa304cef732cb9b87995d0996734340f5673750abfe827af29b875dde7e0b08cf88b345da2e1543b9459879d4072168
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.37.0] - 2026-08-05
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`refresh_tokens.session_id` is finally populated, and the cause was statement ordering, not a missing feature.** Every refresh token in every consuming app had a nil `session_id`, so every session→refresh revocation cascade — the gem's own `Session#revoke!`, `Session.revoke_sessions!`, and each app's equivalent — matched **zero rows**, and 0.35.0's parent-session check had no parent to consult. Measured in production: jumpdrive 15 tokens / 0 linked; nutripod 238 / 0, with **177 live at the time of measurement**.
|
|
15
|
+
|
|
16
|
+
`generate_token_response` created the refresh token one statement *before* `maybe_persist_session_for_token!` ran, inside the same transaction — so when `persist_refresh_token!` wrote `session_id`, there was no session yet to point at. The link was nil **by construction**, not by omission. Swapping the two statements is the fix; `refresh_token_session_id` now returns the session the same grant just materialised.
|
|
17
|
+
|
|
18
|
+
See rarebit-one/rarebit-ops#304.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **BEHAVIOUR NARROWED — `validate_parent_session!` now refuses only on a REVOKED parent, no longer on an expired one.** This is a deliberate relaxation of what 0.35.0 shipped, and hosts should read it before upgrading.
|
|
23
|
+
|
|
24
|
+
`Session#active?` is `!revoked? && !expired?`, so the original `return if session.active?` also refused a refresh whose parent had merely aged out. That was harmless while nothing was linked — with no parent, neither branch could fire. The moment linkage above becomes real, an expiry branch silently becomes a **lifetime policy**, and a badly-scaled one: an app running an 86400s `browser_session_lifetime` against a 2592000s `refresh_token_lifetime` would see its session-backed clients drop from monthly to **daily** re-authorization, as a side effect of a security fix that was never about login frequency.
|
|
25
|
+
|
|
26
|
+
Revocation is the property the estate wants: an operator revokes a session and access ends. Lifetime remains `refresh_token_lifetime`'s job, which already exists and is already configured per host. Keeping them separate is what lets linkage ship without renegotiating anyone's login cadence.
|
|
27
|
+
|
|
28
|
+
- **`ServiceSession` is carved out of linkage on purpose.** Machine credentials keep their own lifecycle, matching the exclusion the `:account` revocation scope already makes, so a human's browser logout cannot kill a running CLI or MCP agent sharing the account.
|
|
29
|
+
|
|
30
|
+
### Notes for hosts
|
|
31
|
+
|
|
32
|
+
**This release does not retroactively repair your app.** Linkage happens only where the host materialises a session at token-issue time via `config.session.session_type_resolver`. A host with no resolver configured keeps nil `session_id` and is unchanged in every respect. The gem now makes the property *achievable and automatic*; enabling it per app is separate work.
|
|
33
|
+
|
|
34
|
+
Existing rows cannot be linked retroactively — there is no record of which session issued them.
|
|
35
|
+
|
|
36
|
+
## [0.36.2] - 2026-08-04
|
|
37
|
+
|
|
38
|
+
**The third release in one chain, and the last of it.** 0.36.0 made the account guards bite on live sessions; 0.36.1 repaired the 500 that exposed on `ApiEngine`; this repairs 0.36.1's own overreach onto the OAuth token endpoint. That reads like flailing, so state it plainly: 0.36.0 was a correct behaviour change, and each follow-up is a narrower blast radius of the same root cause — an exception pair that no controller in the ancestry had ever had to answer for, meeting two route families with two different wire contracts. 0.36.1 fixed the first family and, by rescuing on the shared parent, silently annexed the second. 0.36.2 draws the line between them. Nothing here reverts 0.36.0 or 0.36.1; resource-endpoint behaviour is byte-identical to 0.36.1.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- **`POST /api/oauth/token` answers `400 invalid_grant` again — not `401 invalid_token` — when the account behind the grant is deactivated or locked.** 0.36.1 registered `rescue_from StandardId::AccountDeactivatedError` / `AccountLockedError` on `StandardId::Api::BaseController`, rendering RFC 6750 §3.1's bearer challenge. That is right for a bearer-**protected resource** (`/api/v1/sessions`, `/api/v1/userinfo`): the caller presented an access token and must be told to discard it. But `StandardId::Api::Oauth::BaseController` re-rescues only `OAuthError`, so both account errors fell through to the inherited bearer handler on every OAuth protocol route as well. The token endpoint is not a protected resource — the client is *obtaining* a credential, not presenting one — so there is no token to challenge, and RFC 6749 §5.2 requires a token-endpoint error object instead. A `401` carrying `WWW-Authenticate: Bearer …` is not a legal answer to a grant request, and a conformant client has no defined way to interpret it.
|
|
43
|
+
|
|
44
|
+
`StandardId::Api::Oauth::BaseController` now rescues both errors itself. Rails resolves `rescue_from` handlers most-recently-registered-first and a subclass registers after its parent, so the OAuth handler wins on OAuth routes while `Api::BaseController`'s `401` continues to govern every other API route — unchanged. Specs now pin **both** contracts side by side (`spec/requests/standard_id/api/account_error_response_shape_spec.rb`), since the whole defect was one shape applied to two contracts; the file carries the negative control for each half.
|
|
45
|
+
|
|
46
|
+
- **Why `invalid_grant`, and why it is rendered explicitly.** RFC 6749 §5.2 enumerates six codes. `invalid_grant` — the grant is "invalid, expired, revoked, … or was issued to another client" — is what an authorization code or refresh token belonging to a disabled account has become: the resource owner's authorization no longer stands and no retry will succeed. `unauthorized_client` was the alternative and is wrong on **subject**: it says the *client* is not permitted to use this grant type, a property of client registration that has not changed. The client is fine; the account behind the grant is not. Mapping account state onto a client-scoped code would send well-behaved clients off to audit a registration that is correct.
|
|
47
|
+
|
|
48
|
+
The handler renders that code and status literally rather than routing through `handle_oauth_error`. Both account errors are bare `StandardError`s with no `oauth_error_code` / `http_status`, so `handle_oauth_error`'s `respond_to?` fallbacks would yield `invalid_request` / `400`. The status is right by luck; the code is not — §5.2 reserves `invalid_request` for a **malformed** request, and this request is perfectly well-formed. Worse, the fallback path uses `exception.message`, which would put `"Account is deactivated"` / `"Account has been locked"` straight into `error_description`. Depending on a default that is wrong in two of three fields is a coincidence, not reuse.
|
|
49
|
+
|
|
50
|
+
- **The `error_description` discloses nothing about account state.** Both errors render the same generic sentence — `"The provided authorization grant is invalid, expired or revoked"` — because whoever presents a grant may not be its legitimate holder, which is the exact scenario the account guard exists for. Telling the presenter of a leaked refresh token that the account is suspended is a disclosure, and returning distinguishable text for "deactivated" versus "locked" is a smaller one. `AccountLockedError#lock_reason` is operator-authored text for logs and admin screens and is surfaced in neither the body nor any header; a spec pins that across both. The specific reason belongs in the host's `ACCOUNT_LOCKED` / `ACCOUNT_DEACTIVATED` subscriber, server-side.
|
|
51
|
+
|
|
52
|
+
### Documentation
|
|
53
|
+
|
|
54
|
+
- **README: never guard a `rescue_from` with a `rescue_handlers` check.** This release exists partly because a consumer's own token-endpoint shim was written as `rescue_from X unless StandardId::Api::BaseController.rescue_handlers.any? { … }` — defensive-looking, and a trap. When 0.36.1 registered a handler for that class on the gem superclass, the guard stopped matching, the host's block never registered, and the gem's `401` silently replaced the host's `400` with no error, no deprecation, and nothing in the host's diff. The two failure modes compounded: the gem shipped the wrong shape *and* disabled the shim that would have corrected it. The README now says to register unconditionally — a later registration already outranks the gem's, which is what the guard was groping for. The hazard is the gem's to warn about, since only the gem can create it.
|
|
55
|
+
|
|
10
56
|
## [0.36.1] - 2026-08-04
|
|
11
57
|
|
|
12
58
|
**A follow-up to 0.36.0's own regression, released the same day.** 0.36.0 is correct and stays — read its entry first for why `SESSION_VALIDATING` had to start carrying `account:`. This release repairs the fallout of that fix on the gem's own API routes. Two releases land together because the second is only reachable *because* of the first.
|
data/README.md
CHANGED
|
@@ -788,11 +788,23 @@ When an inactive account attempts to authenticate — at sign-in, at token mint,
|
|
|
788
788
|
|
|
789
789
|
> **What the engine's own routes do.** You are responsible for **your** controllers only. The engine handles its own:
|
|
790
790
|
>
|
|
791
|
-
> - **`ApiEngine` routes** (`/api/v1/sessions`, `/api/v1/userinfo`, …) answer **`401` with `error: "invalid_token"`** per RFC 6750 — `StandardId::Api::BaseController` descends from `ActionController::API`, so your `rescue_from` is not in its ancestry and could never have covered it.
|
|
791
|
+
> - **`ApiEngine` resource routes** (`/api/v1/sessions`, `/api/v1/userinfo`, …) answer **`401` with `error: "invalid_token"`** plus a `WWW-Authenticate` challenge, per RFC 6750 §3.1 — the caller *presented* an access token and must be told to discard it. `StandardId::Api::BaseController` descends from `ActionController::API`, so your `rescue_from` is not in its ancestry and could never have covered it.
|
|
792
|
+
> - **`ApiEngine` OAuth protocol routes** (`POST /api/oauth/token`, `/introspect`, `/revoke`, `/register`) answer **`400` with `error: "invalid_grant"`** per RFC 6749 §5.2 — a client at the token endpoint is *obtaining* a credential, not presenting one, so there is no bearer token to challenge. The `error_description` is deliberately generic (`"The provided authorization grant is invalid, expired or revoked"`) and identical for deactivated and locked accounts: whoever presents a grant may not be its legitimate holder, so the response must not confirm account state. `lock_reason` is never surfaced.
|
|
792
793
|
> - **`WebEngine` routes** (`/sessions`, `/account`, `/logout`) are covered by the `rescue_from` you write on `ApplicationController` below, because `StandardId::Web::BaseController` descends from it. The engine deliberately registers no handler of its own there — one would take precedence over yours and override your UX.
|
|
793
794
|
>
|
|
794
795
|
> So: write the `ApplicationController` handler (it covers your pages *and* the web engine's), and write the `Api::BaseController` handler only for **your own** API controllers.
|
|
795
796
|
|
|
797
|
+
> **⚠️ Never guard a `rescue_from` with a `rescue_handlers` check.** A shim written like this looks defensive and is a trap:
|
|
798
|
+
>
|
|
799
|
+
> ```ruby
|
|
800
|
+
> # DON'T — silently becomes a no-op the moment the gem registers its own handler
|
|
801
|
+
> unless StandardId::Api::BaseController.rescue_handlers.any? { |k, _| k == "StandardId::AccountDeactivatedError" }
|
|
802
|
+
> StandardId::Api::BaseController.rescue_from(StandardId::AccountDeactivatedError) { ... }
|
|
803
|
+
> end
|
|
804
|
+
> ```
|
|
805
|
+
>
|
|
806
|
+
> When a superclass in the gem gains a handler for that class — as `Api::BaseController` did in 0.36.1 — the guard stops matching, your block never registers, and *the gem's* response shape silently replaces yours with no error, no deprecation, and nothing in the diff. Register unconditionally instead: Rails resolves `rescue_from` handlers most-recently-registered-first, so a later registration already wins over the gem's without any guard. This is exactly how one consumer's `400 invalid_grant` token-endpoint shim was defeated by the gem's `401 invalid_token`.
|
|
807
|
+
|
|
796
808
|
```ruby
|
|
797
809
|
# app/controllers/application_controller.rb
|
|
798
810
|
class ApplicationController < ActionController::Base
|
|
@@ -907,7 +919,7 @@ User.unlocked.active # => Users who can log in
|
|
|
907
919
|
|
|
908
920
|
### Handling AccountLockedError
|
|
909
921
|
|
|
910
|
-
When a locked account attempts to authenticate, `StandardId::AccountLockedError` is raised. The error includes metadata about the lock. The same engine-owned-route rules described under [Handling AccountDeactivatedError](#handling-accountdeactivatederror) apply: `ApiEngine` routes answer `401 invalid_token` themselves
|
|
922
|
+
When a locked account attempts to authenticate, `StandardId::AccountLockedError` is raised. The error includes metadata about the lock. The same engine-owned-route rules described under [Handling AccountDeactivatedError](#handling-accountdeactivatederror) apply: `ApiEngine` resource routes answer `401 invalid_token` themselves, `ApiEngine` OAuth protocol routes answer `400 invalid_grant`, and `WebEngine` routes defer to your `ApplicationController` handler. Neither engine-owned response ever echoes `lock_reason`.
|
|
911
923
|
|
|
912
924
|
```ruby
|
|
913
925
|
# app/controllers/application_controller.rb
|
|
@@ -4,6 +4,24 @@ module StandardId
|
|
|
4
4
|
class BaseController < StandardId::Api::BaseController
|
|
5
5
|
rescue_from StandardId::OAuthError, with: :handle_oauth_error
|
|
6
6
|
|
|
7
|
+
# These two are ALREADY rescued on Api::BaseController (since 0.36.1),
|
|
8
|
+
# which renders RFC 6750 §3.1 `401 invalid_token` with a
|
|
9
|
+
# `WWW-Authenticate` challenge. That is the correct answer for a
|
|
10
|
+
# bearer-PROTECTED RESOURCE — /api/v1/sessions, /api/v1/userinfo —
|
|
11
|
+
# where the caller presented a token and must be told to discard it.
|
|
12
|
+
#
|
|
13
|
+
# It is the wrong answer here. Everything under Api::Oauth is an OAuth
|
|
14
|
+
# PROTOCOL endpoint: at /api/oauth/token the client is *obtaining* a
|
|
15
|
+
# credential, not presenting one, so there is no bearer token to
|
|
16
|
+
# challenge and RFC 6749 §5.2 mandates a token-endpoint error object
|
|
17
|
+
# (HTTP 400, `error`, `error_description`) instead. Re-registering the
|
|
18
|
+
# pair here is what keeps the two contracts apart — Rails resolves
|
|
19
|
+
# rescue_from handlers most-recently-registered-first, and a subclass
|
|
20
|
+
# registers after its parent, so these win for OAuth routes only and
|
|
21
|
+
# the resource-endpoint 401 is left exactly as 0.36.1 shipped it.
|
|
22
|
+
rescue_from StandardId::AccountDeactivatedError, with: :handle_account_unusable
|
|
23
|
+
rescue_from StandardId::AccountLockedError, with: :handle_account_unusable
|
|
24
|
+
|
|
7
25
|
private
|
|
8
26
|
|
|
9
27
|
def handle_oauth_error(exception)
|
|
@@ -16,6 +34,49 @@ module StandardId
|
|
|
16
34
|
error_description: description
|
|
17
35
|
}, status: status
|
|
18
36
|
end
|
|
37
|
+
|
|
38
|
+
# `invalid_grant` — chosen deliberately from the RFC 6749 §5.2
|
|
39
|
+
# enumeration, and rendered explicitly rather than by falling through
|
|
40
|
+
# `handle_oauth_error`.
|
|
41
|
+
#
|
|
42
|
+
# WHY invalid_grant. §5.2 defines it as the grant being "invalid,
|
|
43
|
+
# expired, revoked, ... or issued to another client", which is exactly
|
|
44
|
+
# what an authorization code or refresh token belonging to a disabled
|
|
45
|
+
# account has become: the resource owner's authorization no longer
|
|
46
|
+
# stands, and no retry of this grant will ever succeed.
|
|
47
|
+
# `unauthorized_client` was the alternative and is wrong on subject —
|
|
48
|
+
# it says the CLIENT is not permitted to use this grant type, a
|
|
49
|
+
# property of client registration that has not changed here; the
|
|
50
|
+
# client is fine, the account behind the grant is not. Mapping account
|
|
51
|
+
# state onto a client-scoped code would send well-behaved clients off
|
|
52
|
+
# to audit their registration.
|
|
53
|
+
#
|
|
54
|
+
# WHY EXPLICIT, not the respond_to? fallback. AccountDeactivatedError
|
|
55
|
+
# and AccountLockedError are bare StandardErrors with no
|
|
56
|
+
# `oauth_error_code` / `http_status`, so routing them through
|
|
57
|
+
# `handle_oauth_error` would yield `invalid_request` / 400 by default.
|
|
58
|
+
# 400 is right; `invalid_request` is not — §5.2 reserves it for a
|
|
59
|
+
# MALFORMED request (missing parameter, repeated parameter), and this
|
|
60
|
+
# request is perfectly well-formed. It would also leak the raw
|
|
61
|
+
# exception message ("Account is deactivated" / "Account has been
|
|
62
|
+
# locked") into `error_description`, which is precisely what must not
|
|
63
|
+
# happen. Relying on a default that is wrong in both fields is not
|
|
64
|
+
# reuse, it is a coincidence waiting to break.
|
|
65
|
+
#
|
|
66
|
+
# WHY THE DESCRIPTION IS GENERIC. Whoever presents a grant may not be
|
|
67
|
+
# its legitimate holder — a leaked refresh token is the scenario the
|
|
68
|
+
# account guard exists for — so the response must not confirm account
|
|
69
|
+
# state to the presenter. Both errors therefore render the same
|
|
70
|
+
# RFC-shaped sentence, and `AccountLockedError#lock_reason` (operator
|
|
71
|
+
# text for logs and admin screens) is never surfaced. The specific
|
|
72
|
+
# reason belongs in the host's ACCOUNT_LOCKED / ACCOUNT_DEACTIVATED
|
|
73
|
+
# subscriber, server-side.
|
|
74
|
+
def handle_account_unusable(_error)
|
|
75
|
+
render json: {
|
|
76
|
+
error: "invalid_grant",
|
|
77
|
+
error_description: "The provided authorization grant is invalid, expired or revoked"
|
|
78
|
+
}, status: :bad_request
|
|
79
|
+
end
|
|
19
80
|
end
|
|
20
81
|
end
|
|
21
82
|
end
|
|
@@ -84,9 +84,26 @@ module StandardId
|
|
|
84
84
|
#
|
|
85
85
|
# Checking the parent here makes the property true by construction: it
|
|
86
86
|
# holds however the session was revoked — `revoke!`, a bare `update!`, a
|
|
87
|
-
# bulk `update_all`, a DBA, a data fix
|
|
87
|
+
# bulk `update_all`, a DBA, a data fix.
|
|
88
88
|
# See rarebit-one/rarebit-ops#297.
|
|
89
89
|
#
|
|
90
|
+
# REVOCATION, NOT EXPIRY — deliberately, and this is a narrowing of what
|
|
91
|
+
# 0.35.0 shipped. `Session#active?` is `!revoked? && !expired?`, so the
|
|
92
|
+
# original `return if session.active?` also refused a refresh whose parent
|
|
93
|
+
# had merely aged out. That was harmless while `session_id` was nil
|
|
94
|
+
# everywhere; the moment rarebit-one/rarebit-ops#304 makes linkage real it
|
|
95
|
+
# silently becomes a lifetime policy — and a badly-scaled one. jumpdrive
|
|
96
|
+
# runs an 86400s browser_session_lifetime against a 2592000s
|
|
97
|
+
# refresh_token_lifetime, so tying the two would cut its session-backed
|
|
98
|
+
# MCP clients from monthly to DAILY re-authorization, as a side effect of
|
|
99
|
+
# a security fix nobody asked to change login frequency.
|
|
100
|
+
#
|
|
101
|
+
# Revocation is the property the estate actually wants: an operator clicks
|
|
102
|
+
# "revoke this session" and access ends. Expiry is a lifetime decision that
|
|
103
|
+
# belongs to `refresh_token_lifetime`, which already exists and is already
|
|
104
|
+
# configured per app. Keeping them separate means linkage can ship without
|
|
105
|
+
# renegotiating anyone's login cadence.
|
|
106
|
+
#
|
|
90
107
|
# A refresh token with no session (`session_id` nil) is unaffected: that
|
|
91
108
|
# is the machine-to-machine shape (client_credentials and any other grant
|
|
92
109
|
# the gem issues without persisting a session), where there is no parent
|
|
@@ -99,7 +116,7 @@ module StandardId
|
|
|
99
116
|
def validate_parent_session!
|
|
100
117
|
session = @current_refresh_token_record.session
|
|
101
118
|
return if session.nil?
|
|
102
|
-
return
|
|
119
|
+
return unless session.revoked?
|
|
103
120
|
|
|
104
121
|
raise StandardId::InvalidGrantError, "Refresh token is no longer valid"
|
|
105
122
|
end
|
|
@@ -63,8 +63,15 @@ module StandardId
|
|
|
63
63
|
# (see there for why) — that path is safe because the swallowed
|
|
64
64
|
# exception fires before any DB work in this block.
|
|
65
65
|
ActiveRecord::Base.transaction do
|
|
66
|
+
# Session FIRST, refresh token second. The order is load-bearing, not
|
|
67
|
+
# stylistic: `persist_refresh_token!` writes `session_id`, so the
|
|
68
|
+
# session row has to exist before the token row is built or the link
|
|
69
|
+
# is nil by construction. It was the other way round until
|
|
70
|
+
# rarebit-one/rarebit-ops#304, which is the whole reason
|
|
71
|
+
# `refresh_tokens.session_id` was nil in every app in the estate —
|
|
72
|
+
# there was no session to point at yet.
|
|
73
|
+
@oauth_session = maybe_persist_session_for_token!
|
|
66
74
|
response[:refresh_token] = generate_refresh_token if supports_refresh_token?
|
|
67
|
-
maybe_persist_session_for_token!
|
|
68
75
|
end
|
|
69
76
|
|
|
70
77
|
emit_token_issued(expires_in)
|
|
@@ -166,8 +173,33 @@ module StandardId
|
|
|
166
173
|
)
|
|
167
174
|
end
|
|
168
175
|
|
|
176
|
+
# The parent session for the refresh token issued by THIS grant, when the
|
|
177
|
+
# host materialised one via `config.session.session_type_resolver`.
|
|
178
|
+
#
|
|
179
|
+
# Returning a real id here is what makes "revoking a session ends that
|
|
180
|
+
# session's access" true by construction rather than an emergent property
|
|
181
|
+
# of every caller remembering to reach for the right method. With it nil —
|
|
182
|
+
# which it was everywhere before rarebit-one/rarebit-ops#304 — every
|
|
183
|
+
# session→refresh cascade in the gem and in the apps matched zero rows,
|
|
184
|
+
# and 0.35.0's parent-session check had no parent to consult.
|
|
185
|
+
#
|
|
186
|
+
# Still nil where there is genuinely no parent: no resolver configured
|
|
187
|
+
# (the default), or a grant the host does not materialise a session for.
|
|
188
|
+
# That keeps machine-to-machine grants (client_credentials) exactly as
|
|
189
|
+
# they were — there is no session to outlive.
|
|
190
|
+
#
|
|
191
|
+
# ServiceSessions are carved out on purpose. Machine credentials have
|
|
192
|
+
# their own lifecycle, and the gem's `:account` revocation scope already
|
|
193
|
+
# excludes them; linking one here would let a human's browser logout kill
|
|
194
|
+
# a running CLI or MCP agent sharing the account. `OauthSessionPersistence`
|
|
195
|
+
# refuses to build one at all, so this guard is belt-and-braces against a
|
|
196
|
+
# host that reaches around it — cheap, and the failure it prevents is
|
|
197
|
+
# silent.
|
|
169
198
|
def refresh_token_session_id
|
|
170
|
-
nil
|
|
199
|
+
return nil if @oauth_session.nil?
|
|
200
|
+
return nil if @oauth_session.is_a?(StandardId::ServiceSession)
|
|
201
|
+
|
|
202
|
+
@oauth_session.id
|
|
171
203
|
end
|
|
172
204
|
|
|
173
205
|
def previous_refresh_token_record
|
data/lib/standard_id/version.rb
CHANGED