standard_id 0.36.0 → 0.36.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 +50 -0
- data/README.md +21 -2
- data/app/controllers/standard_id/api/base_controller.rb +31 -0
- data/app/controllers/standard_id/api/oauth/base_controller.rb +61 -0
- data/lib/standard_id/errors.rb +25 -0
- 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: e6ea917e3795560901ae1213d21a172e84fb836ecd6885f96f99c138291ffd40
|
|
4
|
+
data.tar.gz: e4582d67367ee6026bc058ffb60a17ee729fb23635318c3acec42a153b0d2b19
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b00ae747d7337e330bf1187f600d8a979c345ea64b72a438657ee28c37d0e685ef00205f4db5726532be0f8db04c3c6386ef98322420ba290999d63b80f9ff80
|
|
7
|
+
data.tar.gz: d7c7f079b61c66c65f7c76d5f214e08f11643359199f2cc8b28192b41eff175431a9d8826c224363f94f71adecbd59ff81ff6dd6594d89c15d5498f558b9aebf
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.36.2] - 2026-08-04
|
|
11
|
+
|
|
12
|
+
**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.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **`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.
|
|
17
|
+
|
|
18
|
+
`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.
|
|
19
|
+
|
|
20
|
+
- **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.
|
|
21
|
+
|
|
22
|
+
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.
|
|
23
|
+
|
|
24
|
+
- **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.
|
|
25
|
+
|
|
26
|
+
### Documentation
|
|
27
|
+
|
|
28
|
+
- **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.
|
|
29
|
+
|
|
30
|
+
## [0.36.1] - 2026-08-04
|
|
31
|
+
|
|
32
|
+
**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.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- **`StandardId::Api::BaseController` now answers `401` — not `500` — when the authenticated account is deactivated or locked.** 0.36.0 made `AccountStatus` / `AccountLocking` fire on a live authenticated request for the first time, which is the intended behaviour. But `AccountDeactivatedError` and `AccountLockedError` are bare `StandardError` subclasses, under neither `InvalidSessionError` nor `OAuthError`, and the API base controller rescued only those three families. So the very change that started refusing a disabled account's bearer token turned every route under `ApiEngine` — `/api/v1/sessions`, `/api/v1/userinfo`, all of it — into an unhandled exception for that account. Every consumer mounting `ApiEngine` inherited it. Before 0.36.0 the guard was inert on that path, so the hole existed but was unreachable; the release is what exposed it.
|
|
37
|
+
|
|
38
|
+
Observed independently in two consumers during the 0.36.0 rollout: `luminality-web`, which has an app-side handler on `Api::ErrorHandling` and so saw only the *gem-owned* routes fail, and `sidekick-web`, which has none and saw its whole API tree fail.
|
|
39
|
+
|
|
40
|
+
A 500 is not a refusal. It carries no `WWW-Authenticate`, tells the client nothing about discarding a token that will never work again, and pages the on-call for a routine account state. Both errors now render the established bearer shape — `401`, `{ "error": "invalid_token", "error_description": … }`, plus the matching `WWW-Authenticate` challenge — via the same `render_bearer_unauthorized!` the other credential failures use. No new response shape was invented.
|
|
41
|
+
|
|
42
|
+
**Why `invalid_token`.** RFC 6750 §3.1 defines exactly three codes. `invalid_token` covers a token "expired, revoked, malformed, or **invalid for other reasons**", and a token whose subject account has been disabled is invalid for one of those other reasons; it also carries the right client instruction — discard the token and re-authenticate — which is precisely correct here, since no retry with this token can ever succeed. `insufficient_scope` (403) would be wrong twice over: nothing about this is a scope failure, and 403 invites a client to keep the token and retry. `invalid_request` (400) would be wrong because the request is perfectly well-formed.
|
|
43
|
+
|
|
44
|
+
`AccountLockedError#lock_reason` is **not** surfaced. It is operator-authored text meant for logs and admin screens, and `WWW-Authenticate` is a quoted string that arbitrary text would break as well as leak. A spec pins that the reason appears in neither the body nor the header.
|
|
45
|
+
|
|
46
|
+
### Unchanged, deliberately — two decisions worth knowing about
|
|
47
|
+
|
|
48
|
+
- **The `WebEngine`'s own routes (`/sessions`, `/account`, `/logout`) get no gem-side handler, and this is not an oversight.** `StandardId::Web::BaseController` descends from the **host's** `ApplicationController`, so the `rescue_from StandardId::AccountDeactivatedError` the README has always told hosts to write already covers those routes. A gem handler there would not add safety, it would remove it: Rails scans `rescue_handlers` most-recently-registered-first, and a subclass registers after its parent, so anything the gem registered on `Web::BaseController` would outrank and silently override the host's — replacing a host's "your account has been deactivated" page with whatever the gem chose. `Api::BaseController` has no such escape hatch; descending from `ActionController::API`, there is nowhere in its ancestry for a host to put a handler, which is exactly why only that side is rescued in the gem.
|
|
49
|
+
|
|
50
|
+
**Hosts must therefore still carry their own `ApplicationController` handler for these two errors.** If you do not have one, a deactivated or locked account hitting a web route — yours or the engine's — is an unhandled exception. That was true before 0.36.0 for the sign-in path and remains true; 0.36.0 simply widened the set of requests that can reach it from "sign-in and token mint" to "any authenticated request". The README's account sections now spell out which routes the engine answers for and which it does not.
|
|
51
|
+
|
|
52
|
+
- **`AccountDeactivatedError` / `AccountLockedError` still descend from `StandardError`, not from `InvalidSessionError`.** Reparenting them was considered as the broader fix — it would make every consumer's existing `rescue_from StandardId::InvalidSessionError` catch them automatically, with no consumer change at all — and rejected on two grounds. First, they mean something genuinely different: an `InvalidSessionError` says the credential is no good and the remedy is to sign in again, while these say the credential is fine and the account is disabled, where signing in again will not help and the user needs to be told so. Consumers who deliberately distinguish the two would lose that distinction silently. Second, and decisively, it would hijack handlers hosts have already written: `Web::BaseController` registers `rescue_from NotAuthenticatedError, InvalidSessionError, with: :redirect_unauthenticated_to_login`, so under the reparented hierarchy a host's `rescue_from AccountDeactivatedError` — registered *earlier*, on the parent — would lose to the gem's subclass registration, and every consumer following the README would start bouncing disabled users to `/login` instead of to their account-disabled page. A patch release that quietly re-routes a documented UX is not a patch. The narrow fix leaves both hierarchies and both sets of host handlers exactly as they were.
|
|
53
|
+
|
|
54
|
+
### Testing
|
|
55
|
+
|
|
56
|
+
`spec/requests/standard_id/live_session_account_guard_spec.rb` gains three API examples (401 shape for deactivated, 401 shape for locked, and `lock_reason` non-disclosure) and two web examples pinning the delegate-to-host design above. Proven by negative control: with the two `rescue_from` lines deleted from `Api::BaseController`, the three API examples fail with the raw error escaping the request; restored, the file is 14/14 and the suite 2097/2097.
|
|
57
|
+
|
|
58
|
+
Relates to rarebit-one/rarebit-ops#306.
|
|
59
|
+
|
|
10
60
|
## [0.36.0] - 2026-08-04
|
|
11
61
|
|
|
12
62
|
### Security
|
data/README.md
CHANGED
|
@@ -784,7 +784,26 @@ User.inactive # => Users with status 'inactive'
|
|
|
784
784
|
|
|
785
785
|
### Handling AccountDeactivatedError
|
|
786
786
|
|
|
787
|
-
When an inactive account attempts to authenticate, `StandardId::AccountDeactivatedError` is raised. You need to handle this error in your application controller:
|
|
787
|
+
When an inactive account attempts to authenticate — at sign-in, at token mint, and (since 0.36.0) on every request made with an already-issued credential — `StandardId::AccountDeactivatedError` is raised. You need to handle this error in your application controller:
|
|
788
|
+
|
|
789
|
+
> **What the engine's own routes do.** You are responsible for **your** controllers only. The engine handles its own:
|
|
790
|
+
>
|
|
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.
|
|
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.
|
|
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.
|
|
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`.
|
|
788
807
|
|
|
789
808
|
```ruby
|
|
790
809
|
# app/controllers/application_controller.rb
|
|
@@ -900,7 +919,7 @@ User.unlocked.active # => Users who can log in
|
|
|
900
919
|
|
|
901
920
|
### Handling AccountLockedError
|
|
902
921
|
|
|
903
|
-
When a locked account attempts to authenticate, `StandardId::AccountLockedError` is raised. The error includes metadata about the lock:
|
|
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`.
|
|
904
923
|
|
|
905
924
|
```ruby
|
|
906
925
|
# app/controllers/application_controller.rb
|
|
@@ -16,6 +16,16 @@ module StandardId
|
|
|
16
16
|
rescue_from StandardId::InvalidSessionError, with: :handle_invalid_session
|
|
17
17
|
rescue_from StandardId::OAuthError, with: :handle_oauth_error
|
|
18
18
|
|
|
19
|
+
# AccountStatus / AccountLocking raise these from the SESSION_VALIDATING
|
|
20
|
+
# subscriber, i.e. mid-request on an ALREADY authenticated call, not just
|
|
21
|
+
# at sign-in or token mint. Unrescued they are a 500 on every gem-owned
|
|
22
|
+
# API route. Unlike Web::BaseController — which descends from the host's
|
|
23
|
+
# ApplicationController, so a host `rescue_from` covers it — this class
|
|
24
|
+
# descends from ActionController::API, so the host has nowhere to put a
|
|
25
|
+
# handler in the ancestry and the gem must answer for itself.
|
|
26
|
+
rescue_from StandardId::AccountDeactivatedError, with: :handle_account_deactivated
|
|
27
|
+
rescue_from StandardId::AccountLockedError, with: :handle_account_locked
|
|
28
|
+
|
|
19
29
|
protected
|
|
20
30
|
|
|
21
31
|
def validate_content_type!
|
|
@@ -43,6 +53,27 @@ module StandardId
|
|
|
43
53
|
render_bearer_unauthorized!(error_description: default_invalid_token_message)
|
|
44
54
|
end
|
|
45
55
|
|
|
56
|
+
# RFC 6750 §3.1 offers exactly three error codes, and `invalid_token` is
|
|
57
|
+
# the right one: it covers a token "expired, revoked, malformed, or
|
|
58
|
+
# invalid for other reasons", and a bearer token whose subject account has
|
|
59
|
+
# been disabled is invalid for one of those other reasons. It also carries
|
|
60
|
+
# the correct client instruction — discard the token and re-authenticate —
|
|
61
|
+
# which is what we want, since no retry with this token will ever succeed.
|
|
62
|
+
# `insufficient_scope` (403) would be wrong: nothing here is about scope,
|
|
63
|
+
# and a 403 invites the client to keep the token and retry. `invalid_request`
|
|
64
|
+
# (400) would be wrong: the request is well-formed.
|
|
65
|
+
def handle_account_deactivated(_error)
|
|
66
|
+
render_bearer_unauthorized!(error_description: "The account is deactivated")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Deliberately does NOT surface `error.lock_reason`. It is operator-authored
|
|
70
|
+
# text intended for logs and admin screens (see StandardId::AccountLockedError),
|
|
71
|
+
# and the WWW-Authenticate header this renders into is a quoted string that
|
|
72
|
+
# arbitrary text would break as well as leak.
|
|
73
|
+
def handle_account_locked(_error)
|
|
74
|
+
render_bearer_unauthorized!(error_description: "The account is locked")
|
|
75
|
+
end
|
|
76
|
+
|
|
46
77
|
def handle_oauth_error(error)
|
|
47
78
|
render json: {
|
|
48
79
|
error: error.oauth_error_code,
|
|
@@ -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
|
data/lib/standard_id/errors.rb
CHANGED
|
@@ -7,6 +7,31 @@ module StandardId
|
|
|
7
7
|
class RevokedSessionError < InvalidSessionError; end
|
|
8
8
|
|
|
9
9
|
# Account errors
|
|
10
|
+
#
|
|
11
|
+
# These deliberately do NOT descend from InvalidSessionError, even though
|
|
12
|
+
# doing so would make every host's existing `rescue_from InvalidSessionError`
|
|
13
|
+
# catch them for free. Two reasons:
|
|
14
|
+
#
|
|
15
|
+
# 1. They mean something different. An InvalidSessionError says the
|
|
16
|
+
# credential is no good — expired, revoked — and the remedy is to sign in
|
|
17
|
+
# again. These say the credential is fine and the ACCOUNT is disabled;
|
|
18
|
+
# signing in again will not help, and the user needs to be told so.
|
|
19
|
+
# Hosts document that distinction in their UX (see the README's
|
|
20
|
+
# "Handling AccountDeactivatedError" / "Handling AccountLockedError").
|
|
21
|
+
# 2. Reparenting them would silently HIJACK the handlers hosts already
|
|
22
|
+
# wrote. Web::BaseController registers
|
|
23
|
+
# `rescue_from NotAuthenticatedError, InvalidSessionError, with:
|
|
24
|
+
# :redirect_unauthenticated_to_login`, and Rails resolves rescue_from
|
|
25
|
+
# handlers most-recently-registered-first — a subclass's registration
|
|
26
|
+
# wins over its parent's. A host's `rescue_from AccountDeactivatedError`
|
|
27
|
+
# on ApplicationController is registered EARLIER than the gem's
|
|
28
|
+
# subclass registration, so as a subclass of InvalidSessionError these
|
|
29
|
+
# would start bouncing to /login instead of reaching the host's
|
|
30
|
+
# account-deactivated page. That is a behaviour regression for every
|
|
31
|
+
# consumer who followed the README.
|
|
32
|
+
#
|
|
33
|
+
# Consequently every entry point that must not raise has to rescue these
|
|
34
|
+
# explicitly; StandardId::Api::BaseController does.
|
|
10
35
|
class AccountDeactivatedError < StandardError; end
|
|
11
36
|
|
|
12
37
|
class AccountLockedError < StandardError
|
data/lib/standard_id/version.rb
CHANGED