standard_id 0.29.0 → 0.30.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 +1038 -0
- data/README.md +50 -0
- data/app/controllers/standard_id/api/oauth/revocations_controller.rb +146 -60
- data/app/controllers/standard_id/web/base_controller.rb +41 -0
- data/app/controllers/standard_id/web/login_controller.rb +1 -1
- data/app/controllers/standard_id/web/login_verify_controller.rb +1 -1
- data/app/controllers/standard_id/web/signup_controller.rb +3 -1
- data/app/models/standard_id/session.rb +42 -0
- data/lib/standard_id/config/schema.rb +39 -0
- data/lib/standard_id/oauth/authorization_code_flow.rb +37 -3
- data/lib/standard_id/version.rb +1 -1
- metadata +2 -1
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,1038 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.30.0] - 2026-07-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`config.oauth.strict_redirect_uri_matching`** (default `false`). RFC 6749
|
|
15
|
+
§4.1.3 requires a `redirect_uri` that was present at `/authorize` to be
|
|
16
|
+
repeated, identically, at `/token`; the token endpoint previously only
|
|
17
|
+
compared the values when the client bothered to send one, so omitting the
|
|
18
|
+
parameter skipped the check entirely. Strictness is opt-in so the flip is
|
|
19
|
+
deliberate. While it is off, a code minted **with** a `redirect_uri` that is
|
|
20
|
+
redeemed **without** one logs a warning naming the `client_id` — watch for
|
|
21
|
+
it, then set the flag to `true`. Codes minted without a `redirect_uri` are
|
|
22
|
+
unaffected either way, and a *mismatched* value has always been (and remains)
|
|
23
|
+
rejected.
|
|
24
|
+
|
|
25
|
+
- **`config.oauth.revocation_scope`** (default `:account`, the existing
|
|
26
|
+
behaviour). `POST /oauth/revoke` bulk-revoked **every** active `DeviceSession`
|
|
27
|
+
for the token's subject regardless of which token was presented, so one
|
|
28
|
+
client's logout signed the account out everywhere. Setting it to `:grant`
|
|
29
|
+
narrows revocation to the authorization grant behind the presented token
|
|
30
|
+
(RFC 7009 §2.1): the refresh-token family its `jti` resolves to, plus that
|
|
31
|
+
grant's `Session` when one is linked. A stateless access token resolves to no
|
|
32
|
+
stored artifact and therefore revokes nothing under `:grant` (still `200`,
|
|
33
|
+
per RFC 7009 §2.2) — present the refresh token. An unrecognised value logs a
|
|
34
|
+
warning and falls back to `:account`. `ServiceSession`s remain untouched
|
|
35
|
+
under both settings. The `OAUTH_TOKEN_REVOKED` event payload gains a
|
|
36
|
+
`refresh_tokens_revoked` count alongside `sessions_revoked` (additive).
|
|
37
|
+
|
|
38
|
+
- **`Session.authenticate_by_token`** / **`Session#authenticate_token`**.
|
|
39
|
+
`Session.by_token` matches only the SHA256 `lookup_hash` — an index key, not
|
|
40
|
+
a credential — so every consumer hand-rolled the mandatory BCrypt verify
|
|
41
|
+
against `token_digest` (and the `BCrypt::Errors::InvalidHash` rescue). The
|
|
42
|
+
new entry point does the lookup and a constant-time digest comparison
|
|
43
|
+
(`ActiveSupport::SecurityUtils.secure_compare`), honours the current scope
|
|
44
|
+
(`Session.api_compatible.active.authenticate_by_token(token)`) and returns
|
|
45
|
+
`nil` rather than raising for blank, unknown, mismatched, or malformed-digest
|
|
46
|
+
tokens.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- `CHANGELOG.md` is now included in the published gem. The gemspec's file glob
|
|
51
|
+
omitted it, so every release to date shipped without one even though the file
|
|
52
|
+
has always existed in the repo — the only gem in the `standard_*` family
|
|
53
|
+
missing it. No code change; `lib/`, `app/`, `config/`, and `db/` are unaffected.
|
|
54
|
+
|
|
55
|
+
## [0.29.1] - 2026-07-16
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- **Signing in during an OAuth flow returned a 500 when `use_inertia` is on.**
|
|
60
|
+
With `config.use_inertia = true`, the WebEngine's auth pages are Inertia
|
|
61
|
+
components, so their form submits are Inertia XHRs. The three
|
|
62
|
+
post-authentication redirects — passwordless OTP verify, password login, and
|
|
63
|
+
signup — used a plain `redirect_to` to send the user on to whatever was
|
|
64
|
+
stashed in `session[:return_to_after_authenticating]`. When that destination
|
|
65
|
+
is a *non*-Inertia controller (the ApiEngine's `/api/authorize` in an OAuth
|
|
66
|
+
round-trip), the Inertia client follows the redirect with the `X-Inertia`
|
|
67
|
+
header still attached, and `inertia_rails`' middleware raises
|
|
68
|
+
`NoMethodError: undefined method 'inertia_configuration'` — a 500 that broke
|
|
69
|
+
MCP/OAuth sign-in outright.
|
|
70
|
+
|
|
71
|
+
The root asymmetry: `inertia_rails` mixes its controller module in via
|
|
72
|
+
`on_load(:action_controller_base)`, so `Web::BaseController`
|
|
73
|
+
(`ActionController::Base`) gets `inertia_configuration` while
|
|
74
|
+
`Api::BaseController` (`ActionController::API`) never does.
|
|
75
|
+
|
|
76
|
+
All three sites now route through a shared `redirect_after_authentication`
|
|
77
|
+
helper on `Web::BaseController`, which uses the existing
|
|
78
|
+
`InertiaSupport#redirect_with_inertia` — already used for social login and
|
|
79
|
+
signup — to emit a 409 + `X-Inertia-Location` so the browser performs a real
|
|
80
|
+
page visit. Note this is keyed on the *request* being Inertia, not on the
|
|
81
|
+
destination being cross-origin: the destination in the OAuth case is
|
|
82
|
+
same-origin, so an external-only check would not have fixed it.
|
|
83
|
+
|
|
84
|
+
The flash notice is now written before redirecting so it survives the
|
|
85
|
+
Inertia branch (which cannot carry `redirect_to`'s `notice:` option).
|
|
86
|
+
|
|
87
|
+
- **Post-auth redirects to allow-listed deep links raised
|
|
88
|
+
`UnsafeRedirectError`.** `safe_destination?` admits configured non-HTTP
|
|
89
|
+
schemes (e.g. `myapp://`), but `redirect_to` rejects them without
|
|
90
|
+
`allow_other_host:`. Now passed conditionally — mirroring
|
|
91
|
+
`ProvidersController` — so Rails' same-origin backstop still applies to
|
|
92
|
+
everything else. `safe_destination?` itself is unchanged.
|
|
93
|
+
|
|
94
|
+
## [0.29.0] - 2026-07-15
|
|
95
|
+
|
|
96
|
+
### Fixed
|
|
97
|
+
|
|
98
|
+
- **A rate-limited GET no longer drives an unbounded redirect loop.** The shared
|
|
99
|
+
rate-limit handler bounced every tripped action to `request.path`. That is
|
|
100
|
+
safe for a POST/PATCH (its sibling GET is a different action), but v0.28.0
|
|
101
|
+
shipped the first rate-limited GETs — the email/phone verification-code
|
|
102
|
+
*confirm* `#show` actions — so a tripped GET redirected to *itself*: the
|
|
103
|
+
browser followed the redirect, re-incremented the counter, and got redirected
|
|
104
|
+
again, an unbounded loop the victim's browser drives that also permanently
|
|
105
|
+
resets the window. The handler now renders a terminal `429 Too Many Requests`
|
|
106
|
+
on GET/HEAD (no redirect), and keeps the existing same-path redirect for
|
|
107
|
+
non-GET web actions. API responses are unchanged.
|
|
108
|
+
- **The `Retry-After` header now reflects the tripped limit's real window.** It
|
|
109
|
+
was hardcoded to 15 minutes, which was 4x too short for every 1-hour limit
|
|
110
|
+
(`verification_start_per_ip`, `password_reset_start_per_ip`, `signup_per_ip`,
|
|
111
|
+
`api_passwordless_start_per_ip`, `dynamic_registration_per_ip`). Each
|
|
112
|
+
`rate_limit` declaration's `within:` is now threaded to the handler (captured
|
|
113
|
+
in the per-limit `with:` closure the instant that limit trips, since
|
|
114
|
+
`ActionController::TooManyRequests` carries no window and a controller may
|
|
115
|
+
declare several limits with different windows), so `Retry-After` matches the
|
|
116
|
+
window that actually fired. A hand-rolled `raise` that bypasses the macro
|
|
117
|
+
falls back to 15 minutes.
|
|
118
|
+
- **Blank per-target rate-limit keys no longer collapse into one shared bucket.**
|
|
119
|
+
Five per-target limiters interpolated a param that can be blank, yielding a
|
|
120
|
+
stable key like `"reset-password:"` — a single global bucket (Rails'
|
|
121
|
+
`.compact` does not drop a non-nil empty string), so blank-target spam from
|
|
122
|
+
one source throttled every legitimate user. Affected: web login (by email),
|
|
123
|
+
API passwordless start (by target), email/phone verification start (by
|
|
124
|
+
target), and password-reset start (by email). Each now falls the key back to
|
|
125
|
+
the remote IP when the target is blank, keeping blank spam bounded per-IP
|
|
126
|
+
without poisoning real targets' buckets — mirroring the existing per-audience
|
|
127
|
+
token limiter's "only count well-formed values" shape.
|
|
128
|
+
|
|
129
|
+
### Added
|
|
130
|
+
|
|
131
|
+
- **Mechanism-agnostic login rate-limit config: `rate_limits.login_per_ip`
|
|
132
|
+
(default 20) and `rate_limits.login_per_email` (default 5).** The login
|
|
133
|
+
`#create` action branches password OR passwordless, so on a passwordless app
|
|
134
|
+
the existing `password_login_per_*` fields actually govern the OTP-send limit
|
|
135
|
+
— a misnomer that led four consumer apps to write false config comments. The
|
|
136
|
+
new names describe the action, not a mechanism. When unset they inherit the
|
|
137
|
+
deprecated `password_login_*` values, so existing behaviour is unchanged; when
|
|
138
|
+
set they win.
|
|
139
|
+
|
|
140
|
+
### Deprecated
|
|
141
|
+
|
|
142
|
+
- **`rate_limits.password_login_per_ip` / `rate_limits.password_login_per_email`
|
|
143
|
+
are deprecated in favour of `login_per_ip` / `login_per_email`.** They remain
|
|
144
|
+
fully honoured (the config schema rejects unknown fields at boot, so a host
|
|
145
|
+
still setting the old names keeps working); the login controller reads the new
|
|
146
|
+
alias and falls back to the deprecated field when the alias is left at its
|
|
147
|
+
default. Follows the `max_attempts` → `max_attempts_per_challenge` alias
|
|
148
|
+
precedent.
|
|
149
|
+
|
|
150
|
+
## [0.28.0] - 2026-07-12
|
|
151
|
+
|
|
152
|
+
### Added
|
|
153
|
+
|
|
154
|
+
- **Rate limiting on the last unprotected auth surfaces.** The web
|
|
155
|
+
password-reset request (`reset_password/start`), password signup, and the
|
|
156
|
+
email/phone code-*confirmation* endpoints now carry Rails-native
|
|
157
|
+
`rate_limit`s, closing email-flooding, account-enumeration,
|
|
158
|
+
account-creation-spam, and distributed code-guessing gaps. New config keys:
|
|
159
|
+
`rate_limits.password_reset_start_per_ip` (10/hr),
|
|
160
|
+
`rate_limits.password_reset_start_per_target` (3/15min), and
|
|
161
|
+
`rate_limits.signup_per_ip` (10/hr); the confirm endpoints reuse
|
|
162
|
+
`otp_verify_per_ip`. All use the existing `RateLimitStore` + centralized 429
|
|
163
|
+
handler.
|
|
164
|
+
- **The `passwordless.retry_delay` OTP-resend cooldown is now enforced.**
|
|
165
|
+
Previously the setting existed but did nothing. A resend for the same target
|
|
166
|
+
within the window (default 30s) is rejected with an `InvalidRequestError`;
|
|
167
|
+
the already-issued code stays valid. Set `retry_delay = 0` to disable.
|
|
168
|
+
|
|
169
|
+
## [0.27.0] - 2026-07-03
|
|
170
|
+
|
|
171
|
+
### Added
|
|
172
|
+
|
|
173
|
+
- **Loopback redirect URIs match on any port for public PKCE clients (RFC 8252
|
|
174
|
+
§7.3).** Native apps receive the authorization response on an ephemeral
|
|
175
|
+
local listener whose port cannot be known at registration time. When a
|
|
176
|
+
public client with `require_pkce` presents a redirect URI whose scheme is
|
|
177
|
+
`http` and whose host is a loopback literal (`127.0.0.1`, `::1`, or
|
|
178
|
+
`localhost`), and a registered redirect URI is likewise a loopback URI, the
|
|
179
|
+
comparison now ignores the port — host and path must still match exactly, so
|
|
180
|
+
`127.0.0.1` and `localhost` do not cross-match (RFC 8252 §8.3 recommends the
|
|
181
|
+
IP literals over `localhost`). Confidential clients and non-loopback URIs
|
|
182
|
+
keep strict scheme+host+port+path matching. Registration-time validation is
|
|
183
|
+
unchanged. Token exchange is unaffected: the redirect URI presented at the
|
|
184
|
+
token endpoint is still compared byte-for-byte against the value stored when
|
|
185
|
+
the code was issued.
|
|
186
|
+
|
|
187
|
+
## [0.26.4] - 2026-06-22
|
|
188
|
+
|
|
189
|
+
### Fixed
|
|
190
|
+
|
|
191
|
+
- **WebEngine controller redirects no longer drop the mount prefix on non-root
|
|
192
|
+
mounts.** Isolated-engine `_path` helpers are mount-relative, and
|
|
193
|
+
`redirect_to` / `redirect_with_inertia` (unlike `form_with` / `url_for` view
|
|
194
|
+
URL generation) do not prepend the mount's `SCRIPT_NAME`. So when the engine
|
|
195
|
+
was mounted at a non-root path (e.g. `mount StandardId::WebEngine => "/auth"`),
|
|
196
|
+
redirects produced prefix-less paths that 404'd — most visibly `POST
|
|
197
|
+
/auth/login` redirecting to `/login_verify` instead of `/auth/login_verify`,
|
|
198
|
+
breaking passwordless login. Form actions were never affected. All affected
|
|
199
|
+
redirects (`login` → `login_verify`; `login_verify` / reset-password →
|
|
200
|
+
`login`; session revoke → `sessions`; account update → `account`; and the
|
|
201
|
+
`after_sign_in` denial bounce) now prepend `request.script_name` via a new
|
|
202
|
+
`engine_path` helper (a no-op for root mounts). Regression test added to
|
|
203
|
+
`spec/integration/multi_mount_spec.rb`.
|
|
204
|
+
|
|
205
|
+
## [0.26.3] - 2026-06-15
|
|
206
|
+
|
|
207
|
+
### Fixed
|
|
208
|
+
|
|
209
|
+
- **Unauthenticated access to a protected web page no longer 500s.** The web
|
|
210
|
+
authentication guard (`require_browser_session!`) raises
|
|
211
|
+
`NotAuthenticatedError` / `InvalidSessionError` (for missing / expired /
|
|
212
|
+
revoked sessions) rather than redirecting. The API base controller rescued
|
|
213
|
+
these, but the web base controller did not — so an unauthenticated request to
|
|
214
|
+
a protected web page (e.g. `/sessions`) surfaced as a 500 instead of bouncing
|
|
215
|
+
to login. The web base controller now rescues both and redirects to the login
|
|
216
|
+
page, preserving the original destination.
|
|
217
|
+
|
|
218
|
+
## [0.26.2] - 2026-06-15
|
|
219
|
+
|
|
220
|
+
### Security
|
|
221
|
+
|
|
222
|
+
- **OTP codes and password-reset tokens no longer leak into the logs.** The
|
|
223
|
+
built-in mailers (`PasswordlessMailer`, `PasswordResetMailer`) pass the OTP
|
|
224
|
+
code / reset URL as mailer params, which `deliver_later` serializes as the
|
|
225
|
+
delivery job's arguments — and ActiveJob's log subscriber prints job arguments
|
|
226
|
+
in plaintext on enqueue/perform (e.g. `params: {email:…, otp_code: "03158369"}`).
|
|
227
|
+
StandardId's mailers now deliver via `StandardId::SecureMailDeliveryJob`
|
|
228
|
+
(`ActionMailer::MailDeliveryJob` with `log_arguments = false`), so the
|
|
229
|
+
arguments are kept out of the log stream. Delivery behaviour is unchanged.
|
|
230
|
+
|
|
231
|
+
## [0.26.1] - 2026-06-15
|
|
232
|
+
|
|
233
|
+
### Fixed
|
|
234
|
+
|
|
235
|
+
- **Passwordless `login_verify` OTP input now respects
|
|
236
|
+
`config.passwordless.code_length`.** The built-in ERB verification-code field
|
|
237
|
+
hardcoded `maxlength: 6`, so apps configuring a longer code (e.g.
|
|
238
|
+
`code_length = 8`) rendered a field that truncated input to 6 characters —
|
|
239
|
+
users could not enter the full code. The input now derives `maxlength` from
|
|
240
|
+
`StandardId::Passwordless.otp_code_length` (the same clamped 4..10 value the
|
|
241
|
+
OTP generator uses), exposed to views via a new
|
|
242
|
+
`StandardId::ApplicationHelper#otp_code_length` helper, so the form and the
|
|
243
|
+
generated code stay in sync end-to-end.
|
|
244
|
+
- **WebEngine rate-limit responses no longer 500 on hosts without a root
|
|
245
|
+
route.** When a web auth action (login, login_verify, email/phone verification
|
|
246
|
+
start) hit its rate limit, the handler redirected to
|
|
247
|
+
`request.referer || main_app.root_path`. That raised — and returned a 500
|
|
248
|
+
error page instead of the intended graceful response — for any host app that
|
|
249
|
+
doesn't define a `root` route (e.g. an API/control-plane that only mounts the
|
|
250
|
+
engine), and also for cross-origin `Referer` headers (Rails' open-redirect
|
|
251
|
+
guard). The handler now redirects back to the rate-limited form's own path
|
|
252
|
+
(`request.path`), which is always a valid same-origin GET. The ApiEngine
|
|
253
|
+
responses (JSON `429`) are unchanged.
|
|
254
|
+
- **OAuth/OIDC metadata no longer advertises a `jwks_uri` under symmetric
|
|
255
|
+
signing.** With the default HS256 (and HS384/HS512) there are no public keys
|
|
256
|
+
to publish, so the JWKS endpoint deliberately returns 404 — but the
|
|
257
|
+
authorization-server and openid-configuration documents advertised `jwks_uri`
|
|
258
|
+
unconditionally, pointing clients at a dead URL. `jwks_uri` is now emitted only
|
|
259
|
+
when signing is asymmetric (RS*/ES*). RFC 8414 makes it optional; HS-signed
|
|
260
|
+
tokens are verified with the shared secret, not JWKS.
|
|
261
|
+
- **Sign-out / unauthenticated requests no longer leave a non-HttpOnly
|
|
262
|
+
`session_token` cookie.** `clear_session!` assigned
|
|
263
|
+
`cookies.encrypted[:session_token] = nil`, which wrote a fresh encrypted blob
|
|
264
|
+
through the cookie jar's default options (no `HttpOnly`) on every
|
|
265
|
+
unauthenticated request. It now uses `cookies.delete(:session_token)` to remove
|
|
266
|
+
the cookie cleanly. The token-bearing sign-in write was already `httponly: true`
|
|
267
|
+
and is unchanged, so this is a hygiene/consistency fix, not a token exposure.
|
|
268
|
+
|
|
269
|
+
## [0.26.0] - 2026-06-15
|
|
270
|
+
|
|
271
|
+
### Added
|
|
272
|
+
|
|
273
|
+
- **`config.passwordless.production_env_detector`** — an optional callable that
|
|
274
|
+
decides whether the current deploy counts as "production" for the bypass-code
|
|
275
|
+
guard. When `nil` (default) the gem falls back to `Rails.env.production?`, so
|
|
276
|
+
existing consumers are unchanged. Apps that distinguish a physical deploy
|
|
277
|
+
environment from `RAILS_ENV` (e.g. a staging box still running
|
|
278
|
+
`RAILS_ENV=production`) can supply `-> { AppEnv.production? }` to permit a
|
|
279
|
+
`bypass_code` on staging while it stays refused on real production. The guard
|
|
280
|
+
in `Passwordless::VerificationService` (which also backs `Otp.verify`) now
|
|
281
|
+
defers to this detector instead of checking `Rails.env.production?` directly.
|
|
282
|
+
|
|
283
|
+
## [0.25.0] - 2026-06-13
|
|
284
|
+
|
|
285
|
+
### Changed
|
|
286
|
+
|
|
287
|
+
- **Browser session cookie now persists across browser restarts.** The
|
|
288
|
+
encrypted `session_token` cookie is written with an explicit `expires` tied
|
|
289
|
+
to the `BrowserSession#expires_at` (previously a bare session cookie that was
|
|
290
|
+
cleared on full browser close, logging users out well before their session
|
|
291
|
+
actually expired). The cookie is also hardened with `httponly: true`,
|
|
292
|
+
`same_site: :lax`, and `secure` following `request.ssl?`. Combined with a
|
|
293
|
+
host-configured `session.browser_session_lifetime`, this lets a "remember me
|
|
294
|
+
for N days" session survive closing and reopening the browser. Applies to
|
|
295
|
+
both the sign-in and remember-token re-auth paths.
|
|
296
|
+
|
|
297
|
+
## [0.24.0] - 2026-06-13
|
|
298
|
+
|
|
299
|
+
### Added
|
|
300
|
+
|
|
301
|
+
- **Public-client (PKCE) support at `POST /oauth/token` for the
|
|
302
|
+
`authorization_code` grant.** Public clients (native/SPA/MCP clients per
|
|
303
|
+
RFC 8252 / OAuth 2.1) can now exchange an authorization code for tokens
|
|
304
|
+
using PKCE alone, with no `client_secret`. Confidential clients still
|
|
305
|
+
authenticate with a secret exactly as before (regression-safe). The flow
|
|
306
|
+
looks up the `ClientApplication` by `client_id`, validates a secret only
|
|
307
|
+
for confidential clients, rejects a public client that sends a
|
|
308
|
+
`client_secret` (`invalid_client`), and **fails closed** when a public
|
|
309
|
+
client's authorization code carries no `code_challenge` — PKCE is the
|
|
310
|
+
client's only authentication factor, so a code minted without one is
|
|
311
|
+
rejected with `invalid_grant`. `"none"` is now advertised in
|
|
312
|
+
`token_endpoint_auth_methods_supported` in both discovery documents.
|
|
313
|
+
- **`oauth.dynamic_registration_default_auth_method` config** (default
|
|
314
|
+
`"none"`). Controls the `token_endpoint_auth_method` applied to clients
|
|
315
|
+
created via RFC 7591 Dynamic Client Registration when the request omits
|
|
316
|
+
one — i.e. whether self-registered clients default to public (PKCE-only)
|
|
317
|
+
or confidential (secret-bearing). Validated at use against
|
|
318
|
+
`none` / `client_secret_basic` / `client_secret_post`; an out-of-range
|
|
319
|
+
value raises `ConfigurationError`. Default preserves existing behaviour.
|
|
320
|
+
|
|
321
|
+
### Fixed
|
|
322
|
+
|
|
323
|
+
- **Consent screen now completes for Inertia-rendered hosts.** When a host
|
|
324
|
+
renders the OAuth consent screen via Inertia (`use_inertia`), the
|
|
325
|
+
approve/deny decision arrives as an Inertia XHR, which cannot follow a 302
|
|
326
|
+
to the external client `redirect_uri` — the browser would hang on the
|
|
327
|
+
consent screen. `ConsentController` now emits an Inertia-Location
|
|
328
|
+
(`409` + `X-Inertia-Location`) for Inertia requests so the client performs a
|
|
329
|
+
hard navigation to the callback, while plain (ERB) form posts keep the
|
|
330
|
+
ordinary redirect. No effect on non-Inertia hosts.
|
|
331
|
+
|
|
332
|
+
## [0.23.0] - 2026-06-12
|
|
333
|
+
|
|
334
|
+
### Added
|
|
335
|
+
|
|
336
|
+
- **Per-audience rate limits at `POST /oauth/token`** — new
|
|
337
|
+
`rate_limits.api_token_per_audience_per_ip` config (Hash of
|
|
338
|
+
audience => max requests per IP per 15 minutes, default `{}`). Lets hosts
|
|
339
|
+
tighten the cap for higher-risk audiences (e.g. a public mobile app) while
|
|
340
|
+
internal/partner audiences keep the global `api_token_per_ip` ceiling. A
|
|
341
|
+
request must pass both its audience cap and the global cap. Implemented as
|
|
342
|
+
an explicit `before_action` counter rather than the Rails `rate_limit`
|
|
343
|
+
DSL: the DSL counts every request reaching the action, and a `by:` block
|
|
344
|
+
returning `nil` does not exempt a request — it collapses into a shared
|
|
345
|
+
bucket keyed without the discriminator, so one audience's rule would
|
|
346
|
+
throttle every other audience's traffic. Only requests that actually
|
|
347
|
+
target a configured audience increment that audience's per-IP counter.
|
|
348
|
+
Exceeding the cap renders the standard `rate_limit_exceeded` JSON error
|
|
349
|
+
with `Retry-After`.
|
|
350
|
+
|
|
351
|
+
### Fixed
|
|
352
|
+
|
|
353
|
+
- **`SOCIAL_AUTH_FAILED` is now emitted on the API (mobile) callback path
|
|
354
|
+
too.** Since 0.16.0 the event fired only from the web callback
|
|
355
|
+
(`Web::Auth::Callback::ProvidersController`); on
|
|
356
|
+
`POST /api/oauth/callback/:provider` an infrastructure-level provider
|
|
357
|
+
failure (`StandardId::OAuthError` from the provider call) fell through to
|
|
358
|
+
the standard `handle_oauth_error` JSON response without emitting, so host
|
|
359
|
+
apps observing provider outages via the event were blind on the API flow
|
|
360
|
+
(and had to monkey-patch `get_user_info_from_provider` to compensate).
|
|
361
|
+
The rescue is scoped to the provider call: `OAuthError` subclasses raised
|
|
362
|
+
later in the flow (`SocialLinkError`, `InvalidRequestError`, ...) are
|
|
363
|
+
policy/client errors and still do not emit. The JSON error response is
|
|
364
|
+
unchanged.
|
|
365
|
+
|
|
366
|
+
## [0.22.0] - 2026-06-11
|
|
367
|
+
|
|
368
|
+
### Added
|
|
369
|
+
|
|
370
|
+
- **RFC 7591 Dynamic Client Registration** behind a default-off toggle. New
|
|
371
|
+
endpoint `POST /oauth/register`
|
|
372
|
+
(`Api::Oauth::RegistrationsController` -> `StandardId::Oauth::ClientRegistration`)
|
|
373
|
+
lets clients self-register OAuth client applications.
|
|
374
|
+
- **Rate limited.** Throttled by IP via
|
|
375
|
+
`rate_limits.dynamic_registration_per_ip` (default 10/hour) so an enabled
|
|
376
|
+
deployment can't be flooded with client rows.
|
|
377
|
+
- **Default off.** Gated on `oauth.dynamic_registration_enabled` (default
|
|
378
|
+
`false`). While off, the endpoint returns **404** (fully absent, not just a
|
|
379
|
+
guarded 403) and the discovery documents do **not** advertise a
|
|
380
|
+
`registration_endpoint`. An open, unauthenticated registration endpoint is
|
|
381
|
+
state-mutating attack surface, so it is strictly opt-in.
|
|
382
|
+
- **Owner resolver.** When enabled, set
|
|
383
|
+
`oauth.dynamic_registration_owner` to a callable resolving the polymorphic
|
|
384
|
+
owner for registered clients (e.g. `-> { Organization.default }`). If the
|
|
385
|
+
toggle is on but the resolver is nil/returns nil, registration raises a
|
|
386
|
+
clear `StandardId::ConfigurationError` rather than silently failing model
|
|
387
|
+
validation.
|
|
388
|
+
- **Metadata -> ClientApplication mapping** (RFC 7591 §2): `redirect_uris`
|
|
389
|
+
(REQUIRED — empty/invalid yields `invalid_redirect_uri`), `client_name` ->
|
|
390
|
+
`name` (a name is generated when absent), `scope` (default
|
|
391
|
+
`"openid profile email"`). `grant_types` is whitelisted to
|
|
392
|
+
`authorization_code`/`refresh_token` and `response_types` to `code` (others
|
|
393
|
+
rejected as `invalid_client_metadata`). `token_endpoint_auth_method` `none`
|
|
394
|
+
-> **public** client; `client_secret_basic`/`client_secret_post` ->
|
|
395
|
+
**confidential** (a one-time `client_secret` is generated and returned with
|
|
396
|
+
`client_secret_expires_at: 0`). Default auth method is `none` (public).
|
|
397
|
+
- **Forced security defaults.** All registered clients are forced onto
|
|
398
|
+
`require_pkce: true` + `code_challenge_methods: "S256"` (the model also
|
|
399
|
+
validates this for public clients). Registered clients default to
|
|
400
|
+
`require_consent: true` — they get the HTML consent screen shipped in
|
|
401
|
+
0.21.0 rather than the old `require_consent: false` workaround.
|
|
402
|
+
- **Discovery advertisement.** Both `/.well-known/openid-configuration` and
|
|
403
|
+
`/.well-known/oauth-authorization-server` advertise
|
|
404
|
+
`registration_endpoint` **only when** `oauth.dynamic_registration_enabled`
|
|
405
|
+
is true (the flag is now read from config and passed into
|
|
406
|
+
`DiscoveryDocument.build`).
|
|
407
|
+
- Responses follow RFC 7591 §3.2.1 (HTTP 201) on success and §3.2.2 (HTTP
|
|
408
|
+
400, `invalid_redirect_uri` / `invalid_client_metadata`) on error. No
|
|
409
|
+
migration required — all `ClientApplication` columns already exist.
|
|
410
|
+
|
|
411
|
+
## [0.21.1] - 2026-06-11
|
|
412
|
+
|
|
413
|
+
### Fixed
|
|
414
|
+
|
|
415
|
+
- **ERB login view now respects the `web.signup` and `web.password_reset`
|
|
416
|
+
toggles.** The packaged login view rendered an unconditional "Sign up" link
|
|
417
|
+
(both the passwordless and password branches) and a "Forgot password?" link
|
|
418
|
+
(password branch), so an app with `web.signup = false` or
|
|
419
|
+
`web.password_reset = false` showed links to routes that 404. The links are
|
|
420
|
+
now gated on their respective toggles. No effect on apps that leave the
|
|
421
|
+
toggles at their defaults.
|
|
422
|
+
|
|
423
|
+
## [0.21.0] - 2026-06-11
|
|
424
|
+
|
|
425
|
+
### Added
|
|
426
|
+
|
|
427
|
+
- **RFC 8414 OAuth 2.0 Authorization Server Metadata** — new endpoint
|
|
428
|
+
`/.well-known/oauth-authorization-server` (`Api::WellKnown::OauthAuthorizationServerController`),
|
|
429
|
+
serving the same document as `/.well-known/openid-configuration`. Both
|
|
430
|
+
controllers now render a single shared builder,
|
|
431
|
+
`StandardId::Oauth::DiscoveryDocument.build(issuer, registration_enabled: false)`,
|
|
432
|
+
so the OIDC and OAuth metadata documents cannot drift.
|
|
433
|
+
- **Mount caveat:** the ApiEngine is consumer-mounted at a sub-path (e.g.
|
|
434
|
+
`/auth/api`), so the gem can only serve this at
|
|
435
|
+
`/auth/api/.well-known/oauth-authorization-server`. A strict RFC 8414 client
|
|
436
|
+
that derives a *root-anchored* URL from a path-carrying issuer
|
|
437
|
+
(`<host>/.well-known/oauth-authorization-server/auth/api`) lands outside any
|
|
438
|
+
engine mount; hosts needing the root-anchored form must add their own root
|
|
439
|
+
route — the gem cannot. The `registration_endpoint` is intentionally NOT
|
|
440
|
+
emitted yet; the `registration_enabled:` kwarg is a seam for Phase 2 (DCR).
|
|
441
|
+
- **PKCE advertisement** — both discovery documents now advertise
|
|
442
|
+
`code_challenge_methods_supported: ["S256"]` (always on; PKCE is always
|
|
443
|
+
enforced).
|
|
444
|
+
- **HTML consent view for the authorization-code flow** — an authenticated,
|
|
445
|
+
interactive (HTML) `/authorize` for a client with `require_consent` enabled
|
|
446
|
+
and no prior grant is now handed off to a new WebEngine consent screen
|
|
447
|
+
(`GET/POST /consent`, asset-free ERB; Inertia consumers receive props for
|
|
448
|
+
their own component) instead of dead-ending. On approve, a `ClientGrant` is
|
|
449
|
+
recorded and the authorization code is issued by re-running the same
|
|
450
|
+
authorization-code flow (so `redirect_uri` and PKCE are revalidated, not
|
|
451
|
+
duplicated); on deny, the user is redirected back with `error=access_denied`
|
|
452
|
+
(+ `state`). Repeat authorizations with a matching grant skip consent. The
|
|
453
|
+
API authorize endpoint carries the original `/authorize` params to the
|
|
454
|
+
consent screen through a signed, expiring payload
|
|
455
|
+
(`StandardId::Oauth::ConsentPayload`, mirroring the OTP `message_verifier`
|
|
456
|
+
pattern). New table `standard_id_client_grants` (one row per account+client).
|
|
457
|
+
JSON / non-interactive / implicit / social-login flows are unaffected.
|
|
458
|
+
|
|
459
|
+
### Changed
|
|
460
|
+
|
|
461
|
+
- **`audience` is now OPTIONAL at the authorization-code `/authorize`** — moved
|
|
462
|
+
from `expect_params` to `permit_params` in
|
|
463
|
+
`AuthorizationCodeAuthorizationFlow`. Token-time validation already no-ops on a
|
|
464
|
+
blank audience (or when no `allowed_audiences` are configured), so omitting it
|
|
465
|
+
is safe and lets standards-compliant clients (e.g. MCP) authorize without it.
|
|
466
|
+
`client_credentials` still REQUIRES `audience` (unchanged). This is a
|
|
467
|
+
relaxation, not a break.
|
|
468
|
+
- **Passwordless-aware ERB login view** — the gem's ERB login view now selects
|
|
469
|
+
its form using the same passwordless-first precedence the controller's
|
|
470
|
+
`#create` uses: passwordless-only renders an asset-free email-only form (no
|
|
471
|
+
external `tailwindcss.com` logo, no Tailwind-utility dependence, so it renders
|
|
472
|
+
under a minimal element-CSS layout); password mode renders the existing form
|
|
473
|
+
unchanged (password consumers are unaffected); neither-enabled renders a "No
|
|
474
|
+
login method is enabled" message instead of a 500. Social login still renders
|
|
475
|
+
in both modes when configured.
|
|
476
|
+
|
|
477
|
+
### Migration notes
|
|
478
|
+
|
|
479
|
+
- Run the new `CreateStandardIdClientGrants` migration (adds
|
|
480
|
+
`standard_id_client_grants`). No existing columns change.
|
|
481
|
+
|
|
482
|
+
## [0.20.1] - 2026-05-24
|
|
483
|
+
|
|
484
|
+
### Added
|
|
485
|
+
|
|
486
|
+
- **`after_sign_in` hook context now includes `:redirect_uri`** — the caller-supplied destination (from the form param for password/signup flows, from the OAuth state cookie for social flows, from `session[:return_to_after_authenticating]` for passwordless OTP). Host hooks that always return a default path (e.g. `PostLoginRedirect.new(account).path`) silently shadowed the caller's `redirect_uri` because `redirect_override` wins in the destination chain. Hooks can now return `nil` when `context[:redirect_uri]` is present so the originator's URL is honoured — required for OAuth/SSO flows where the host bounces through `/login?redirect_uri=/oauth/authorize?…` and expects the handshake to complete back to the originating consumer (e.g. external API clients hitting `/api/v1/authorize`).
|
|
487
|
+
- **All four sign-in flows now forward the caller's redirect_uri into the hook context**: password login, signup, social callback, AND passwordless OTP verify (`web/login_verify_controller.rb`). Previously only the first three were covered; passwordless users initiating OAuth from a consumer landed on the host default page.
|
|
488
|
+
|
|
489
|
+
### Fixed
|
|
490
|
+
|
|
491
|
+
- **Cancel-at-provider preserves `redirect_uri`** — `handle_callback_error` (provider returns `?error=access_denied`) now extracts the state and forwards `redirect_uri` to `login_path`, symmetric with the `SocialLinkError`/`OAuthError` rescue paths. Previously a user who cancelled at the provider lost the OAuth handshake context entirely.
|
|
492
|
+
- **Open-redirect / 500 mitigation in social callback** — when the host hook defers (returns nil), the social callback validates the originator-supplied destination via `safe_destination?` (same-origin paths or `allowed_redirect_url_prefixes` matches only; rejects protocol-relative and arbitrary cross-host URLs). On failure, falls back to `/` instead of feeding the unsafe value into `redirect_to`. Closes a class of phishing vectors that opened when host hooks started deferring instead of always returning an internal path.
|
|
493
|
+
- **`params[:redirect_uri]` Array/Hash type safety** — login, signup, login_verify, and logout controllers now use a `string_param` helper that returns nil for non-String shapes (e.g. `redirect_uri[]=a&redirect_uri[]=b`), preventing a self-DoS 500 from `redirect_to <Array>`. Covers all `params[:redirect_uri]` read sites: form re-renders (`show`/error branches), session writes (`handle_passwordless_login`), state encoding (`signup_controller#encode_state`), and direct redirects (`logout`). Also normalizes empty-string values via `.presence` consistently across the context and destination chain.
|
|
494
|
+
- **Open-redirect validation extended to password, signup, logout, AND passwordless verify** — `safe_destination?` and `safe_post_signin_default` are now promoted to `Web::BaseController` and applied to the destination chain for password login, password signup, logout, and passwordless OTP verify (was previously social callback only). `safe_destination?` accepts same-origin absolute URLs (compares against `request.base_url`) so legitimate `store_location_for_redirect` round-trips still work. Cross-host URLs not in `allowed_redirect_url_prefixes`, protocol-relative URLs (`//evil.com/`), and same-origin-looking-but-cross-host URLs (e.g. `http://evil.com:80/`) fall back to `after_authentication_url` / `safe_post_signin_default` instead of redirecting to an attacker-controlled target. Closes a residual open-redirect in passwordless verify where a malicious String redirect_uri passed `string_param` (which only blocks Array/Hash) and got stashed in `session[:return_to_after_authenticating]`, then served unfiltered.
|
|
495
|
+
- **Scope-level `after_sign_in_path` no longer shadows caller's redirect_uri** — when the host hook returns nil AND `context[:redirect_uri]` is present, `LifecycleHooks#invoke_after_sign_in` now returns nil (the documented "defer to originator" signal) instead of `scope_config&.after_sign_in_path`. Hosts that configure both a scope path AND OAuth/SSO flows previously had the scope path silently win and break the handshake.
|
|
496
|
+
- **Defensive nil-guard on `state_data['redirect_uri']`** in the social callback — uses `state_data&.dig("redirect_uri").presence` consistent with the rescue paths.
|
|
497
|
+
|
|
498
|
+
## [0.20.0] - 2026-05-21
|
|
499
|
+
|
|
500
|
+
### Changed (BREAKING — behavior)
|
|
501
|
+
|
|
502
|
+
- **OAuth token grants now fail closed when the requested audience has a configured profile binding but the account has no matching active profile.** Previously, `TokenGrantFlow` only validated `aud ∈ allowed_audiences`; if `c.oauth.audience_profile_types[aud]` was set but the account lacked a matching profile, the mint silently succeeded with profile-derived claims (e.g. `gid`) resolving to `nil`. The new behavior raises `StandardId::NoBoundProfileError` (a subclass of `InvalidGrantError`), which the standard OAuth error handler renders as RFC 6749 `invalid_grant` (HTTP 400). Decode-time enforcement via `AudienceVerification` is unchanged.
|
|
503
|
+
- **`AudienceProfileResolver` now exposes a strict `.resolve!(account:, audience:)` method** used by `TokenGrantFlow`. It returns the uniquely matching active profile, or raises `NoBoundProfileError` (no match) / `AmbiguousProfileError` (multiple matches). The legacy `.call(account:, audience:)` is unchanged — it still returns the "first active else first match" profile and is used by the decode-time `AudienceVerification` concern, where back-compat tolerance is intentional.
|
|
504
|
+
|
|
505
|
+
### Added
|
|
506
|
+
|
|
507
|
+
- `StandardId::NoBoundProfileError` and `StandardId::AmbiguousProfileError` — both subclass `InvalidGrantError` so existing OAuth error handlers map them to `invalid_grant`. Exposed readers (`audience`, `expected_profile_types`, `profile_ids`) are for audit logging only; do **not** interpolate them into client-facing responses.
|
|
508
|
+
- **`OAUTH_TOKEN_ISSUED` event payload now includes** `profile_id`, `audience`, `jti`, and `requested_scopes` (in addition to the existing `grant_type`, `client_id`, `account`, `expires_in`). Without these, downstream subscribers (SIEM, audit log, anomaly detection) could not correlate a successful mint to the entity it authorized, the resource server it targeted, the specific token for revocation, or the scopes the client requested. Existing subscribers are unaffected — payload additions are backward-compatible.
|
|
509
|
+
- `claim_resolvers_context` now exposes the pre-resolved `profile` (when a binding matched), so host-app claim resolvers can use it directly via keyword filtering instead of re-querying.
|
|
510
|
+
|
|
511
|
+
### Migration notes
|
|
512
|
+
|
|
513
|
+
Host apps with **multiple active profiles of the same type for a single account** will see previously-silent mints now fail with `AmbiguousProfileError`. Two options:
|
|
514
|
+
|
|
515
|
+
1. **Recommended:** Treat duplicates as a data-integrity bug and deactivate the superfluous profiles. The previous "pick the first arbitrary active match" behavior was non-deterministic across reloads and unsafe to rely on.
|
|
516
|
+
2. **Temporary:** Configure a custom `c.oauth.audience_profile_resolver` callable that applies your own selection rule. The strict path delegates to it when set.
|
|
517
|
+
|
|
518
|
+
An explicit per-grant `profile_id` parameter is intentionally out of scope for this release; the grant-parameter contract for profile selection will be designed separately once host apps have migrated off duplicate profiles.
|
|
519
|
+
|
|
520
|
+
## [0.19.0] - 2026-05-19
|
|
521
|
+
|
|
522
|
+
### Added
|
|
523
|
+
|
|
524
|
+
- **`Api::Oauth::Callback::ProvidersController` now forwards non-OAuth request params** to `SOCIAL_AUTH_COMPLETED` subscribers as `original_request_params`. Previously the API (mobile) flow always passed an empty hash, blocking host-app attribution tracking for mobile signups. Reserved OAuth/Rails keys (`id_token`, `code`, `scope`, `scopes`, `audience`, `redirect_uri`, `flow`, `state`, `nonce`, `provider`, `controller`, `action`, `format`, `authenticity_token`, `utf8`, `_method`) are stripped; everything else is treated as opaque host-supplied data and forwarded through. Mirrors the web flow's existing `state_data` pass-through contract.
|
|
525
|
+
|
|
526
|
+
## [0.18.0] - 2026-05-19
|
|
527
|
+
|
|
528
|
+
### Changed
|
|
529
|
+
|
|
530
|
+
- Relaxed `jwt` dependency constraint from `~> 2.7` to `>= 2.7, < 4`, allowing consumers to satisfy the GHSA security advisory for `jwt` 2.x by upgrading to `jwt` 3.x. Existing 2.x users are unaffected. Consuming apps that bump to `jwt` 3.x should verify their own JWT encode/decode call sites against the [jwt 3.0 release notes](https://github.com/jwt/ruby-jwt/blob/main/CHANGELOG.md) — `JWT.encode` / `JWT.decode` calls inside `StandardId::JwtService` already pass an explicit algorithm and are 3.x-compatible.
|
|
531
|
+
|
|
532
|
+
## [0.17.1] - 2026-05-07
|
|
533
|
+
|
|
534
|
+
### Fixed
|
|
535
|
+
|
|
536
|
+
- **`Otp.issue(delivery: :manual)` no longer double-delivers when `c.passwordless.delivery == :built_in`.** Previously, `BaseStrategy#start!` emitted `PASSWORDLESS_CODE_GENERATED` unconditionally and `PasswordlessDeliverySubscriber` gated only on the global delivery config — so callers who passed `delivery: :manual` and delivered the code themselves (custom widget/verification/step-up flows) silently received a duplicate email from the bundled mailer on top of their own. `skip_sender` is now forwarded into the event payload, and the subscriber short-circuits when it sees the flag. Manual callers get exactly one delivery again, in line with the documented contract for `:manual`. (#206)
|
|
537
|
+
|
|
538
|
+
## [0.17.0] - 2026-04-29
|
|
539
|
+
|
|
540
|
+
### Changed
|
|
541
|
+
|
|
542
|
+
- Release workflow migrated to the shared `rarebit-one/.github` reusable workflow (`reusable-gem-release.yml@v1`); `.github/workflows/release.yml` is now a thin shim. CI workflow remains bespoke pending unrelated open PRs that touch it.
|
|
543
|
+
- **Widened Rails constraint to `>= 8.0`** — gemspec now allows Rails 9+ when available. Aligns with the org-wide policy of supporting Rails 8 and up with no upper bound.
|
|
544
|
+
- Replaced the vendored `StandardConfig` schema/manager (~430 LOC across `lib/standard_config/`) with `ActiveSupport::OrderedOptions` plus a small internal `StandardId::ConfigSchema` helper (~200 LOC). No public API change for consumers using `StandardId.configure { |c| ... }` or `StandardId.config.foo`. The top-level `StandardConfig` constant has been removed — it was internal-only and shipped under standard_id's lib path, but its name implied a separate gem and risked namespace collisions.
|
|
545
|
+
|
|
546
|
+
### Added
|
|
547
|
+
|
|
548
|
+
- **Rails edge CI canary** — a non-blocking `test (rails-edge)` job runs the spec suite against `rails/rails@main` on every PR. Failures surface upstream breakage during development rather than at a host app's `bundle update` after a Rails 9 release. Allowed to fail (`continue-on-error: true`) so it never blocks merges.
|
|
549
|
+
|
|
550
|
+
### Fixed
|
|
551
|
+
|
|
552
|
+
- **Weekly maintenance concurrency guard** — added a `concurrency:` block to `weekly-maintenance.yml` so a manual `workflow_dispatch` during an in-flight scheduled run no longer spawns a parallel job. `cancel-in-progress: false` lets the running job finish rather than orphan a half-open PR. Follow-up to #199.
|
|
553
|
+
|
|
554
|
+
### Removed
|
|
555
|
+
|
|
556
|
+
- **BREAKING:** Dropped support for Ruby < 4.0. `required_ruby_version` is now `>= 4.0`. Hosts must upgrade to Ruby 4.0+ before bundling this version. CI tests all four published 4.0.x patches.
|
|
557
|
+
|
|
558
|
+
## [0.16.1] - 2026-04-19
|
|
559
|
+
|
|
560
|
+
### Performance
|
|
561
|
+
|
|
562
|
+
- **API authentication guard reuses `session_manager.current_account`** — `Api::AuthenticationGuard` previously ran its own `find_by(id: api_session.account_id)` twice per bearer-authenticated request (once each for `SESSION_VALIDATED` and `SESSION_EXPIRED`), on top of the session_manager's already-memoized `current_account`. The guard now threads `session_manager` through to the event emitters and delegates account resolution to it. Eliminates 1-2 redundant queries per API request. (#188)
|
|
563
|
+
- **`RefreshToken#revoke_family!` uses a recursive CTE** — family chain traversal was a Ruby loop doing `.pluck(:id)` per generation (O(depth) queries). Now a single recursive CTE collects every ancestor and descendant in one round trip. `UNION` (not `UNION ALL`) deduplicates against the full accumulator to prevent infinite loops on cyclic data. Supported by PostgreSQL, SQLite 3.8+, and MySQL 8+. (#188)
|
|
564
|
+
- **`Api::SessionsController#serialize_session` drops redundant `respond_to?` guards** — all `Session` subclasses share the STI table, so per-field `respond_to?` checks were defensive overhead with no missing method to defend against. Direct column access is both cheaper and clearer. (#188)
|
|
565
|
+
|
|
566
|
+
### Added
|
|
567
|
+
|
|
568
|
+
- **`config.session.token_digest_cost`** — opt-in BCrypt cost factor for session `token_digest`. Default `nil` preserves current behavior (bcrypt-ruby's built-in default — cost 12 in production). Since session tokens are 256-bit random (`SecureRandom.urlsafe_base64(32)`), any cost `>= 10` is well beyond brute-force, and setting `10` saves ~200ms of CPU per session creation. Clamped to `BCrypt::Engine::MIN_COST..MAX_COST`. (#188)
|
|
569
|
+
- **Current request details mirrored into `Rails.event` context** — host apps observing structured logs/events see the same `request_id`, `remote_ip`, and `user_agent` values that StandardId records on sessions, without needing to duplicate the wiring. (#187)
|
|
570
|
+
|
|
571
|
+
## [0.16.0] - 2026-04-19
|
|
572
|
+
|
|
573
|
+
### Security
|
|
574
|
+
|
|
575
|
+
- **OTP verification race-condition fix and per-challenge brute-force defenses** — `VerificationService.verify` now wraps the challenge lookup, failed-attempt increment, and consumption in a single `SELECT ... FOR UPDATE` transaction, closing the TOCTOU window between "find active challenge" and "mark it used." Failed-attempt counting is now atomic and scoped to the specific challenge (previously a loose read-modify-write on the account). Events are deferred to post-commit so observers never see rolled-back state. New `config.passwordless.max_attempts_per_challenge` (default `5`) supersedes the now-deprecated account-wide `max_attempts` (kept as a fallback for existing installs). (#169)
|
|
576
|
+
- **JWT audience enforcement at decode time** — `JwtService.decode` now accepts an `allowed_audiences:` kwarg and raises `StandardId::InvalidAudienceError` on mismatch. `Api::TokenManager#verify_jwt_token` threads `config.oauth.allowed_audiences` through automatically, so cross-audience JWT replay is now blocked even on controllers that forget to include the `AudienceVerification` concern. Production emits a warning when `allowed_audiences` is unset. (#170, #174)
|
|
577
|
+
- **Web flow polish** — password-reset delivery moved to an async job with a constant-time success response (closes enumeration timing leak); OAuth `redirect_uri` validation tightened to exact scheme+host+port+path match at both registration and authorize time (blocks query-string piggyback); engine logs a warning when the host app has no `secret_key_base` configured so encrypted session cookies can't silently fall back to plaintext. New `reset_password` config scope with `:delivery` (`:custom` default, `:built_in` opt-in) and mailer-sender/subject knobs. `CREDENTIAL_PASSWORD_RESET_INITIATED` event now fires from the job. (#171)
|
|
578
|
+
- **Per-client PKCE enforcement at the authorize endpoint** — honors the existing `require_pkce` column on `ClientApplication`. Requests missing `code_challenge` are rejected with `invalid_request` when the client requires PKCE. Per-client `code_challenge_methods` replaces the global S256-only hardcode (case-insensitive). New validation blocks public clients from opting out (`public_clients_must_require_pkce`). (#175)
|
|
579
|
+
- **Hardened GitHub Actions workflows** — minimal `permissions:` blocks added to every workflow; third-party actions pinned to commit SHAs. (#185)
|
|
580
|
+
|
|
581
|
+
### Added
|
|
582
|
+
|
|
583
|
+
- **Typed identifier accessors on `AccountAssociations`** — `account.email_identifier`, `account.phone_number_identifier`, `account.username_identifier` replace the manual `identifiers.detect { |i| i.type == "…" }` pattern used by consuming apps. Uses loaded-association detection to stay N+1-safe. (#180)
|
|
584
|
+
- **`SOCIAL_AUTH_FAILED` event** — emitted when social provider callbacks catch `StandardId::OAuthError` from an infrastructure failure (DNS, SSL, timeout). Policy/link errors (`SocialLinkError`) emit their existing `SOCIAL_LINK_BLOCKED` event instead. Enables host apps to observe provider outages without monkey-patching. (#180)
|
|
585
|
+
- **Idempotent event subscriptions** in `AccountStatus` / `AccountLocking` — guarded with a module-level flag so re-including a concern (e.g., Rails reload) no longer accumulates duplicate subscribers. (#180)
|
|
586
|
+
- **Errors module eager-loaded from all engines** — `StandardId::SocialLinkError` and the full error hierarchy are available at engine load time, so `rescue_from StandardId::SocialLinkError` at controller class-body time resolves as a constant instead of needing a string literal. (#180)
|
|
587
|
+
- **`bin/dev`** — dummy-app boot script for contributors; provisions the SQLite dev DB via `rake app:db:setup` if absent and execs `spec/dummy/Procfile.dev` through overmind/hivemind/foreman. (#173)
|
|
588
|
+
- **Boot-time config validators** — new `StandardId::Config::CallableValidator` and `StandardId::Config::ScopeClaimsValidator` raise `StandardId::ConfigurationError` at engine `after_initialize` if lifecycle callables have wrong arity or if `scope_claims` entries reference claims without a matching resolver. Surfaces typos at deploy time instead of at callback time. (#173)
|
|
589
|
+
- **Cleanup rake tasks** — `standard_id:cleanup:{sessions,refresh_tokens,authorization_codes,code_challenges,all}` honoring `GRACE_DAYS` env var, plus `docs/OPERATIONS.md` with scheduling examples for SolidQueue recurring, sidekiq-cron, whenever, and cron. (#173)
|
|
590
|
+
|
|
591
|
+
### Performance
|
|
592
|
+
|
|
593
|
+
- **`first_sign_in?` uses `.exists?` instead of `.count`** on the `LifecycleHooks` hot path — removes a full count on every login. (#172)
|
|
594
|
+
- **Bulk session revocation uses `update_all`** in `Api::OAuth::RevocationsController` — one SQL UPDATE instead of O(N) per-row UPDATEs across sessions + cascaded refresh_tokens. `SESSION_REVOKED` event emission preserved per-session. (#172)
|
|
595
|
+
- **Partial indexes on hot active-row lookups** — `standard_id_sessions(expires_at) WHERE revoked_at IS NULL`, same for `refresh_tokens`, and `code_challenges(realm, channel, target, created_at) WHERE used_at IS NULL`. Dropped the unused Postgres GIN index on `code_challenges.metadata`. Migration uses `algorithm: :concurrently` with `disable_ddl_transaction!` on Postgres. (#172)
|
|
596
|
+
- **Isolate `SESSION_REVOKED` subscriber failures during bulk revoke** — a failing subscriber no longer aborts the revocation loop. (#172)
|
|
597
|
+
|
|
598
|
+
### Deprecated
|
|
599
|
+
|
|
600
|
+
- **`config.passwordless.max_attempts`** — use `max_attempts_per_challenge` instead. The old key is still read as a fallback when the new one is unset, so existing installs keep working. Planned for removal in 2.0. (#169)
|
|
601
|
+
|
|
602
|
+
### Changed
|
|
603
|
+
|
|
604
|
+
- **OTP code format now allows leading zeros** — `StandardId::Passwordless.generate_otp_code` (new consolidated generator, replacing the inline generators in `VerifyEmail::StartController`, `VerifyPhone::StartController`, and `BaseStrategy`) produces codes in the range `[0, 10**n)` zero-padded to the configured length, so values like `"000123"` are now valid. The previous generators produced integers in `[10**(n-1), 10**n)`, which never had leading zeros. Entropy is unchanged; host apps that stored or displayed codes as integers should treat them as strings. (#169)
|
|
605
|
+
|
|
606
|
+
### Chore
|
|
607
|
+
|
|
608
|
+
- Deleted stale top-level `test_authorization_flows.rb` scaffolding. (#173)
|
|
609
|
+
|
|
610
|
+
## [0.15.0] - 2026-04-18
|
|
611
|
+
|
|
612
|
+
### Added
|
|
613
|
+
|
|
614
|
+
- **`StandardId::Otp` public primitive** — New realm-parameterized module (`Otp.issue` / `Otp.verify`) that wraps the hardened passwordless `VerificationService`. Enables OTP flows outside authentication (e.g. contact-verification widgets) without reimplementing enumeration defense, atomic failed-attempt tracking, or the `bypass_code` E2E hook. Supports `:built_in`, `:custom`, and `:manual` delivery modes. (#181)
|
|
615
|
+
- **`JwtService.sign` / `.verify` primitives** — Low-level JWT encode/decode that don't consult config, useful for HS256 service-to-service tokens and similar use cases. Existing `encode` / `decode` / `decode_session` methods unchanged — use those for OAuth flows. Typed error hierarchy under `StandardId::InvalidTokenError` (`ExpiredTokenError`, `InvalidSignatureError`, `InvalidAlgorithmError`, `InvalidAudienceTokenError`). (#177)
|
|
616
|
+
- **`session_type_resolver` callback** — New `config.session.session_type_resolver` decides whether web/API/OAuth sign-ins produce a `BrowserSession`, `DeviceSession`, or `ServiceSession`. Default mirrors current selection. OAuth token grants can now optionally persist a session row (opt-in via the resolver). (#182)
|
|
617
|
+
- **`audience_profile_types` map + audience-aware claim resolvers** — New `config.oauth.audience_profile_types` maps each audience to an allowed profile type (or array), enforced automatically in `AudienceVerification`. `claim_resolvers` now receive `audience:` in their context (via `CallableParameterFilter`), so resolvers can branch per audience. New `OAUTH_AUDIENCE_MISMATCH` event and `InvalidAudienceProfileError`. (#179)
|
|
618
|
+
- **Multi-profile-type scopes + per-scope `authorizer` + `scope_resolver` callback** — Scope config accepts `profile_types:` (plural array) in addition to legacy `profile_type:` singular (deprecated but still works). Each scope may declare an `authorizer:` callable for role-based / wildcard logic that runs after the profile-type check. New `config.scope_resolver` detaches scope resolution from the URL convention — apps using alternate URL schemes (e.g. `control_plane` param) no longer need to override `current_scope_config`. (#178)
|
|
619
|
+
- **Cleanup jobs for authorization codes + code challenges** — `CleanupExpiredAuthorizationCodesJob` and `CleanupExpiredCodeChallengesJob` with dual grace windows (7-day for expired, 1-day for consumed/used — replay-forensics only). Full `standard_id:cleanup:{sessions,refresh_tokens,authorization_codes,code_challenges,all}` rake task set. (#183)
|
|
620
|
+
- **Multi-step install generator** — `rails g standard_id:install` now writes the initializer with grouped sections, appends `mount StandardId::WebEngine` / `ApiEngine` to `config/routes.rb`, auto-runs `rake standard_id:install:migrations`, and prints a post-install checklist pointing at `AccountAssociations`, `WebAuthentication`/`ApiAuthentication`, and cleanup jobs. Flags: `--skip-initializer`, `--skip-routes`, `--skip-migrations`. Idempotent on re-run. (#176)
|
|
621
|
+
|
|
622
|
+
### Deprecated
|
|
623
|
+
|
|
624
|
+
- **`ScopeConfig#profile_type` singular** — Use `profile_types:` (plural) instead. Singular still accepted, emits an `ActiveSupport::Deprecation` warning. Planned for removal in 2.0. (#178)
|
|
625
|
+
|
|
626
|
+
## [0.14.4] - 2026-04-14
|
|
627
|
+
|
|
628
|
+
### Fixed
|
|
629
|
+
|
|
630
|
+
- **Prevent OTP race condition with multiple active challenges** — When a user requests a new OTP before the previous one expires, multiple active challenges could accumulate. The verification lookup returned an arbitrary match, causing valid codes to be rejected. Now invalidates existing active challenges when creating a new one, with ordered lookup as a defensive fallback. Adds composite index on `code_challenges` for the new query pattern. (#165)
|
|
631
|
+
|
|
632
|
+
### Changed
|
|
633
|
+
|
|
634
|
+
- Bump puma from 7.2.0 to 8.0.0 (#162)
|
|
635
|
+
- Group all Dependabot updates including majors (#163)
|
|
636
|
+
|
|
637
|
+
## [0.14.3] - 2026-04-02
|
|
638
|
+
|
|
639
|
+
### Fixed
|
|
640
|
+
|
|
641
|
+
- **Guard `apply_skips!` against unloaded `ControllerPolicy`** — When `skip_host_authorization` is called from a Rails initializer, `ControllerPolicy` may not be autoloaded yet by Zeitwerk, causing a `NameError`. The method now checks `defined?` before accessing the constant. Controllers that register later still receive skips via the `apply_to_controller` callback and the `to_prepare` re-run. (#160)
|
|
642
|
+
|
|
643
|
+
## [0.14.2] - 2026-04-02
|
|
644
|
+
|
|
645
|
+
### Fixed
|
|
646
|
+
|
|
647
|
+
- **Lowercase controller name in Inertia component name generation** — `inertia_component_name` produced PascalCase names like `"standard_id/Login/show"` because `.demodulize` preserves class casing. Adding `.underscore` produces `"standard_id/login/show"` which matches the lowercase page file conventions used by consuming apps. (#158)
|
|
648
|
+
|
|
649
|
+
## [0.14.1] - 2026-03-28
|
|
650
|
+
|
|
651
|
+
### Fixed
|
|
652
|
+
|
|
653
|
+
- **Preserve TLS SNI hostname in SSRF-protected connections** — The SSRF protection layer now preserves the original hostname for TLS Server Name Indication (SNI), preventing certificate verification failures when connecting through resolved IP addresses. (#154)
|
|
654
|
+
|
|
655
|
+
## [0.14.0] - 2026-03-26
|
|
656
|
+
|
|
657
|
+
### Added
|
|
658
|
+
|
|
659
|
+
- **Configurable `username_validator` for passwordless flows** — New `config.passwordless.username_validator` callable that runs before OTP generation to validate the recipient address (e.g. via truemail). Returns nil/false to proceed, or an error message string to reject with `InvalidRequestError`. Follows the same pattern as `account_factory` and `before_sign_in` hooks. (#150)
|
|
660
|
+
- **Integration tests for multi-mount WebEngine with scope defaults (RAR-93)** — Comprehensive integration tests verifying multiple WebEngine mounts with independent scopes, session tracking, and lifecycle hooks. (#149)
|
|
661
|
+
|
|
662
|
+
## [0.13.0] - 2026-03-26
|
|
663
|
+
|
|
664
|
+
### Added
|
|
665
|
+
|
|
666
|
+
- **Scope-aware lifecycle hooks (RAR-95, RAR-96)** — Named authentication scopes with profile-type gating. `ScopeConfig` and `StandardId.scope_for(name)` define scopes; lifecycle hooks receive scope context. Built-in profile validation runs before custom `before_sign_in` hooks, raising `AuthenticationDenied` when required profile is missing. Configurable `profile_resolver` and per-scope `no_profile_message`. (#145)
|
|
667
|
+
- **Multi-scope session tracking (RAR-97)** — `sign_in_account` accepts `scope_name:` and accumulates scopes in `session[:standard_id_scopes]`. New `current_scope_names` helper exposed to controllers and views. Scopes preserved across session fixation reset, cleared on logout. OAuth callback scope preserved via `state_data`. (#146)
|
|
668
|
+
- **Reusable `PasswordlessFlow` concern (RAR-94)** — Public concern wrapping `PasswordlessStrategy` with `generate_passwordless_otp(username:)` and `verify_passwordless_otp(username:)`. YARD documentation added to `WebAuthentication`, `LifecycleHooks`, and `PasswordlessFlow` for host app adoption. `handle_authentication_denied` falls back gracefully when WebEngine is not mounted. (#147)
|
|
669
|
+
|
|
670
|
+
## [0.12.0] - 2026-03-25
|
|
671
|
+
|
|
672
|
+
### Added
|
|
673
|
+
|
|
674
|
+
- CI-driven gem publishing via GitHub Actions trusted publisher
|
|
675
|
+
|
|
676
|
+
## [0.11.0] - 2026-03-25
|
|
677
|
+
|
|
678
|
+
### Added
|
|
679
|
+
|
|
680
|
+
- **Pre-authentication lifecycle hook (RAR-73)** — `before_sign_in` callback for pre-session gating. Supports `AuthenticationDenied` rejection before session creation across all auth paths. (#137)
|
|
681
|
+
- Test coverage thresholds with SimpleCov (RAR-27) (#136)
|
|
682
|
+
|
|
683
|
+
## [0.10.0] - 2026-03-24
|
|
684
|
+
|
|
685
|
+
### Security
|
|
686
|
+
|
|
687
|
+
- **Rate limiting on all auth endpoints (RAR-51, RAR-60, RAR-56)** — Add Rails 8 built-in `rate_limit` to password login, OTP verification, email/phone verification code generation, API passwordless, and API token endpoints. Configurable limits via `rate_limits` config scope. Includes `RateLimitStore` for lazy cache resolution and `RateLimitHandling` concern for graceful 429 responses with `Retry-After` header. (#129)
|
|
688
|
+
|
|
689
|
+
### Added
|
|
690
|
+
|
|
691
|
+
- **Post-authentication lifecycle hooks (RAR-73)** — `after_sign_in` and `after_account_created` configurable callbacks. Support redirect overrides, `AuthenticationDenied` rejection, and `first_sign_in?` detection across all auth paths (password, passwordless, social, signup). (#131)
|
|
692
|
+
- **Passwordless account factory callback (RAR-71)** — `passwordless.account_factory` config callable receives `identifier:`, `params:`, `request:` and replaces default `find_or_create_account!` logic. Runs inside transaction for rollback protection. Eliminates monkey-patching in host apps. (#130)
|
|
693
|
+
- **Passwordless registration flow in WebEngine (RAR-74)** — `web.passwordless_registration` config enables automatic account creation during passwordless login. Fires `PASSWORDLESS_ACCOUNT_CREATED` event. Challenge preserved on rejection for retry. (#131)
|
|
694
|
+
- **Extensible JWT session struct (RAR-68)** — Session struct gains `claims` field with full decoded JWT payload. New `oauth.custom_claims` config callable for encoding custom claims into access tokens. Reserved JWT keys protected from override. (#132)
|
|
695
|
+
- **Built-in OTP email delivery (RAR-63)** — `PasswordlessMailer` with HTML + text templates. `passwordless.delivery` config (`:custom` default / `:built_in`), `mailer_from`, `mailer_subject`. Eliminates ~15 lines of event subscriber boilerplate per host app. (#133)
|
|
696
|
+
- **Reusable OTP verification API (RAR-45)** — `StandardId::Passwordless.verify` public method for host apps with custom controllers. Result object with `error_code` symbols (`:invalid_code`, `:expired`, `:max_attempts`, `:not_found`, `:blank_code`, `:account_not_found`, `:server_error`). (#134)
|
|
697
|
+
- `find_existing_account` method on passwordless strategies for account lookup without creation
|
|
698
|
+
- `RateLimitStore` lazy-resolving cache wrapper for rate limiting infrastructure
|
|
699
|
+
|
|
700
|
+
## [0.9.0] - 2026-03-10
|
|
701
|
+
|
|
702
|
+
### Security
|
|
703
|
+
|
|
704
|
+
- **Database-backed refresh token revocation with rotation and reuse detection (RAR-49)** — Refresh tokens are now stored in the database with token digest, expiry, and revocation tracking. Each refresh rotates the token (old one revoked, new one issued). Reuse of a rotated token triggers family-wide revocation and emits `OAUTH_REFRESH_TOKEN_REUSE_DETECTED` event.
|
|
705
|
+
- **Enforce PKCE S256 only, reject plain method (RAR-50)** — The insecure PKCE `plain` method is no longer accepted. Only `S256` is supported, per OAuth 2.1 best practices.
|
|
706
|
+
- **Hash PKCE code_challenge at storage time (RAR-58)** — The `code_challenge` column now stores a SHA256 hex digest instead of the raw challenge value, for defense-in-depth against database compromise.
|
|
707
|
+
- **Secure password strength defaults (RAR-59)** — `require_special_chars`, `require_uppercase`, and `require_numbers` now default to `true`. Apps that intentionally want weaker passwords must explicitly set `false`.
|
|
708
|
+
|
|
709
|
+
### Added
|
|
710
|
+
|
|
711
|
+
- `StandardId::RefreshToken` model with token digest, expiry, revocation, and family chain tracking
|
|
712
|
+
- `StandardId::CleanupExpiredRefreshTokensJob` for periodic cleanup of expired/revoked refresh tokens
|
|
713
|
+
- `StandardId::PasswordStrength` concern for config-driven password complexity validation
|
|
714
|
+
- `OAUTH_REFRESH_TOKEN_REUSE_DETECTED` security event
|
|
715
|
+
- Session `revoke!` now cascades revocation to associated refresh tokens
|
|
716
|
+
- Session `before_destroy` revokes active refresh tokens before deletion
|
|
717
|
+
|
|
718
|
+
### Changed
|
|
719
|
+
|
|
720
|
+
- **Breaking**: PKCE `plain` method no longer accepted — clients must use `S256`
|
|
721
|
+
- **Breaking**: Password complexity defaults changed from `false` to `true`
|
|
722
|
+
- Refresh tokens now include `jti` claim for database lookup; legacy tokens without `jti` are handled gracefully during migration period
|
|
723
|
+
|
|
724
|
+
### Migration Required
|
|
725
|
+
|
|
726
|
+
```bash
|
|
727
|
+
rails standard_id:install:migrations
|
|
728
|
+
rails db:migrate
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
## [0.8.1] - 2026-03-24
|
|
732
|
+
|
|
733
|
+
### Security
|
|
734
|
+
|
|
735
|
+
- **Fix social login account takeover via implicit email linking (RAR-46)** — When a social login returned an email matching an existing identifier from a different provider, the system granted access without verification. Now validates provider ownership with a configurable `link_strategy` (`:strict` default blocks cross-provider linking, `:trust_provider` preserves legacy behavior)
|
|
736
|
+
- Add SSRF protection to `HttpClient` — resolve hostnames before connecting and reject private/loopback IP ranges; fix DNS rebinding by pinning connections to resolved IPs; validate URL scheme (http/https only)
|
|
737
|
+
- Add session fixation protection — call `reset_session` before creating authenticated browser sessions on both login and remember-me flows
|
|
738
|
+
- Filter sensitive OAuth parameters (`code_verifier`, `code_challenge`, `client_secret`, `id_token`, `refresh_token`, `access_token`, `state`, `nonce`, `authorization_code`) from Rails logs via engine initializer
|
|
739
|
+
|
|
740
|
+
### Added
|
|
741
|
+
|
|
742
|
+
- `StandardId::SocialLinkError` exception with `email` and `provider_name` attributes for host apps to build custom error responses
|
|
743
|
+
- `social.link_strategy` config option (`:strict` or `:trust_provider`)
|
|
744
|
+
- `SOCIAL_LINK_BLOCKED` event in both `SOCIAL_EVENTS` and `SECURITY_EVENTS`
|
|
745
|
+
- `provider` column on `standard_id_identifiers` table (nullable, backfilled on social re-login)
|
|
746
|
+
- `SsrfError` exception class for blocked internal requests
|
|
747
|
+
|
|
748
|
+
### Migration Required
|
|
749
|
+
|
|
750
|
+
```bash
|
|
751
|
+
rails standard_id:install:migrations
|
|
752
|
+
rails db:migrate
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
## [0.8.0] - 2026-03-23
|
|
756
|
+
|
|
757
|
+
### Added
|
|
758
|
+
|
|
759
|
+
- Post-authentication lifecycle hooks: `after_sign_in` and `after_account_created` config callbacks for host apps to run custom logic after authentication events (#118)
|
|
760
|
+
- `StandardId::AuthenticationDenied` exception for rejecting sign-ins from hooks, with automatic session revocation and redirect
|
|
761
|
+
- Configurable auth mechanism toggles for WebEngine via `config.web.*` scope — selectively enable/disable password login, passwordless OTP, social login, signup, password reset, email/phone verification, and session management (#119)
|
|
762
|
+
- `WebMechanismGate` concern with `requires_web_mechanism` class method for controller-level enforcement
|
|
763
|
+
- `first_sign_in?` helper in `LifecycleHooks` concern using active session count
|
|
764
|
+
|
|
765
|
+
### Fixed
|
|
766
|
+
|
|
767
|
+
- Orphaned accounts when `after_sign_in` raises `AuthenticationDenied` during signup or social login — newly created accounts are now cleaned up atomically (#120)
|
|
768
|
+
- Race condition in social login: `RecordNotUnique` on concurrent requests is now rescued with retry
|
|
769
|
+
- `first_sign_in?` now only counts active sessions (excludes expired/revoked)
|
|
770
|
+
- Hardcoded `connection: "email"` in passwordless verify now uses `@otp_data[:connection]`
|
|
771
|
+
- `enforce_web_mechanism!` validates mechanism names with `respond_to?` for actionable errors on typos
|
|
772
|
+
- Removed obsolete brakeman ignore entries, added ignores for hook-controlled redirects
|
|
773
|
+
|
|
774
|
+
### Deprecated
|
|
775
|
+
|
|
776
|
+
- `passwordless.enabled` config field — use `web.passwordless_login` instead
|
|
777
|
+
|
|
778
|
+
## [0.7.1] - 2026-03-20
|
|
779
|
+
|
|
780
|
+
### Added
|
|
781
|
+
|
|
782
|
+
- Configurable `bypass_code` for E2E testing of passwordless verification flows (#113)
|
|
783
|
+
|
|
784
|
+
## [0.7.0] - 2026-03-19
|
|
785
|
+
|
|
786
|
+
### Added
|
|
787
|
+
|
|
788
|
+
- `POST /oauth/revoke` endpoint for RFC 7009-compliant token revocation (#108)
|
|
789
|
+
- `GET /.well-known/openid-configuration` endpoint for OIDC discovery (#109)
|
|
790
|
+
- `GET /api/sessions` and `DELETE /api/sessions/:id` endpoints for mobile session management (#110)
|
|
791
|
+
- `OAUTH_TOKEN_REVOKED` event published on successful token revocation
|
|
792
|
+
|
|
793
|
+
## [0.6.0] - 2026-03-19
|
|
794
|
+
|
|
795
|
+
### Added
|
|
796
|
+
|
|
797
|
+
- `Account.find_or_create_by_verified_email!` class method for race-safe account creation with verified email identifiers (#107)
|
|
798
|
+
- Publishes `ACCOUNT_CREATING` and `ACCOUNT_CREATED` lifecycle events during account creation
|
|
799
|
+
- Auto-sets `email` column on Account if it exists and isn't already provided
|
|
800
|
+
|
|
801
|
+
### Changed
|
|
802
|
+
|
|
803
|
+
- Social OAuth callback now only accepts `scope` (singular) parameter per OAuth 2.0 spec; the `scopes` (plural) fallback has been removed (#106)
|
|
804
|
+
|
|
805
|
+
## [0.5.2] - 2026-03-17
|
|
806
|
+
|
|
807
|
+
### Added
|
|
808
|
+
|
|
809
|
+
- Configurable `sentry_context` lambda for host apps to supply extra Sentry user context fields (email, username, etc.) without overriding the concern method (#98)
|
|
810
|
+
|
|
811
|
+
### Fixed
|
|
812
|
+
|
|
813
|
+
- Rescue `ArgumentError` in `skip_host_authorization` when controllers inherit ActionPolicy but haven't called `verify_authorized` (#98)
|
|
814
|
+
- Guard `sentry_context` lambda against nil returns, non-Hash returns, and non-callable config values (#98)
|
|
815
|
+
- Base Sentry context keys (`id`, `session_id`) cannot be overridden by the lambda (#98)
|
|
816
|
+
|
|
817
|
+
## [0.5.1] - 2026-03-17
|
|
818
|
+
|
|
819
|
+
### Fixed
|
|
820
|
+
|
|
821
|
+
- Use `skip_verify_authorized` for ActionPolicy framework in `skip_host_authorization`, with `respond_to?` guard for API controllers that don't include ActionPolicy (#96)
|
|
822
|
+
- Guard `SentryContext` against sessions without `id` method (#95)
|
|
823
|
+
|
|
824
|
+
### Changed
|
|
825
|
+
|
|
826
|
+
- Bump production dependencies (#94)
|
|
827
|
+
|
|
828
|
+
## [0.5.0] - 2026-03-13
|
|
829
|
+
|
|
830
|
+
### Added
|
|
831
|
+
|
|
832
|
+
- Engine scope context to events for richer event payloads (#91)
|
|
833
|
+
- Nonce parameter support through authorization code flow (#89)
|
|
834
|
+
|
|
835
|
+
### Changed
|
|
836
|
+
|
|
837
|
+
- Automate GitHub Releases from CHANGELOG.md on tag push (#90)
|
|
838
|
+
|
|
839
|
+
## [0.4.0] - 2026-03-12
|
|
840
|
+
|
|
841
|
+
### Added
|
|
842
|
+
|
|
843
|
+
- Controller auth-skip declarations and `StandardId.skip_host_authorization` for authorization gem integration (RAR-64) (#85)
|
|
844
|
+
- `StandardId::PasswordlessVerificationService` for custom passwordless login UIs (RAR-65) (#84)
|
|
845
|
+
- `StandardId::Testing` support package for host app test suites (RAR-66) (#74)
|
|
846
|
+
|
|
847
|
+
### Fixed
|
|
848
|
+
|
|
849
|
+
- Thread-safe class-level memoization in `JwtService` and `PasswordFlow` (#87)
|
|
850
|
+
|
|
851
|
+
## [0.3.2] - 2026-03-11
|
|
852
|
+
|
|
853
|
+
### Added
|
|
854
|
+
|
|
855
|
+
- Optional `SentryContext` concern for enriching Sentry error reports with auth context (#76)
|
|
856
|
+
- Optional `current_user` alias for `current_account` (#75)
|
|
857
|
+
- `account_scope` configuration for eager-loading account associations (#77)
|
|
858
|
+
|
|
859
|
+
### Fixed
|
|
860
|
+
|
|
861
|
+
- Normalize IPv6 localhost (`::1`) to `127.0.0.1` for consistent IP handling (#79)
|
|
862
|
+
|
|
863
|
+
### Changed
|
|
864
|
+
|
|
865
|
+
- Add repo hygiene files: CONTRIBUTING, SECURITY, CODE_OF_CONDUCT (#78)
|
|
866
|
+
|
|
867
|
+
## [0.3.1] - 2026-03-11
|
|
868
|
+
|
|
869
|
+
### Added
|
|
870
|
+
|
|
871
|
+
- Bearer token extraction concern for flexible JWT authentication (RAR-48)
|
|
872
|
+
- JWT audience (`aud`) verification on token decode (RAR-48)
|
|
873
|
+
- Expired session cleanup job with configurable grace period (RAR-62)
|
|
874
|
+
- Boot-time warning when JWT issuer is not configured (RAR-54)
|
|
875
|
+
|
|
876
|
+
### Fixed
|
|
877
|
+
|
|
878
|
+
- Social login now checks provider `email_verified` field before marking emails as verified (RAR-47)
|
|
879
|
+
- Prevent user enumeration via timing side-channel on password login with dummy bcrypt comparison (RAR-53)
|
|
880
|
+
- Replace database error leak in signup with generic message to prevent account enumeration (RAR-61)
|
|
881
|
+
- Remove `account` attribute from `AccountLockedError` to prevent sensitive data exposure (RAR-57)
|
|
882
|
+
- Add HTTP client timeouts (5s open, 10s read) to prevent resource exhaustion from slow OAuth providers (RAR-52)
|
|
883
|
+
- Cap token lifetimes at 24h (access) and 90d (refresh) with log warnings on clamping (RAR-55)
|
|
884
|
+
|
|
885
|
+
## [0.3.0] - 2026-03-10
|
|
886
|
+
|
|
887
|
+
### Added
|
|
888
|
+
|
|
889
|
+
- Passwordless OTP login flow for WebEngine (RAR-44)
|
|
890
|
+
- Audit logging documentation for integration with `standard_audit` gem
|
|
891
|
+
|
|
892
|
+
### Fixed
|
|
893
|
+
|
|
894
|
+
- Disable `strict_loading` on `current_account` in session managers
|
|
895
|
+
|
|
896
|
+
### Changed
|
|
897
|
+
|
|
898
|
+
- Upgrade to Ruby 4.0.1
|
|
899
|
+
- Standardize GitHub Actions workflows and lefthook git hooks
|
|
900
|
+
- Bump dependencies: sqlite3, puma, brakeman, rspec-rails, shoulda-matchers
|
|
901
|
+
|
|
902
|
+
## [0.2.9] - 2025-12-02
|
|
903
|
+
|
|
904
|
+
### Fixed
|
|
905
|
+
|
|
906
|
+
- Add `alg` and `use` fields to JWKS endpoint response
|
|
907
|
+
|
|
908
|
+
## [0.2.8] - 2025-11-28
|
|
909
|
+
|
|
910
|
+
### Added
|
|
911
|
+
|
|
912
|
+
- Signing key rotation with zero-downtime support (CORE-164)
|
|
913
|
+
|
|
914
|
+
## [0.2.7] - 2025-11-20
|
|
915
|
+
|
|
916
|
+
### Added
|
|
917
|
+
|
|
918
|
+
- Basic auth support for client secret authentication
|
|
919
|
+
- Redirect to `login_page` when not logged in
|
|
920
|
+
|
|
921
|
+
## [0.2.6] - 2025-11-15
|
|
922
|
+
|
|
923
|
+
### Added
|
|
924
|
+
|
|
925
|
+
- JWKS endpoint for JWT public key exposure (SWE-701)
|
|
926
|
+
|
|
927
|
+
## [0.2.5] - 2025-11-10
|
|
928
|
+
|
|
929
|
+
### Added
|
|
930
|
+
|
|
931
|
+
- Store `aud` on refresh tokens and expose via `current_session`
|
|
932
|
+
|
|
933
|
+
## [0.2.4] - 2025-11-05
|
|
934
|
+
|
|
935
|
+
### Added
|
|
936
|
+
|
|
937
|
+
- Refresh token support for social OAuth flow
|
|
938
|
+
|
|
939
|
+
## [0.2.3] - 2025-10-30
|
|
940
|
+
|
|
941
|
+
### Added
|
|
942
|
+
|
|
943
|
+
- Scope parameter support in social provider token exchange (SWE-697)
|
|
944
|
+
|
|
945
|
+
## [0.2.2] - 2025-10-25
|
|
946
|
+
|
|
947
|
+
### Added
|
|
948
|
+
|
|
949
|
+
- Action Cable authentication support
|
|
950
|
+
|
|
951
|
+
## [0.2.1] - 2025-10-20
|
|
952
|
+
|
|
953
|
+
### Added
|
|
954
|
+
|
|
955
|
+
- Login params support in OAuth sign-in flow
|
|
956
|
+
|
|
957
|
+
## [0.2.0] - 2025-10-15
|
|
958
|
+
|
|
959
|
+
### Added
|
|
960
|
+
|
|
961
|
+
- Account activation/deactivation with event-driven side effects
|
|
962
|
+
- Account locking/unlocking for administrative security
|
|
963
|
+
- Configurable session expiration
|
|
964
|
+
- Event-driven architecture replacing single callbacks
|
|
965
|
+
|
|
966
|
+
### Changed
|
|
967
|
+
|
|
968
|
+
- Refactor social provider to prepare for plugin architecture
|
|
969
|
+
- Extract Apple and Google providers into separate gems (`standard_id-apple`, `standard_id-google`)
|
|
970
|
+
- Make gem thread-safe for multi-threaded servers
|
|
971
|
+
- Ensure event payloads are audit-ready for external subscribers
|
|
972
|
+
|
|
973
|
+
## [0.1.7] - 2025-09-15
|
|
974
|
+
|
|
975
|
+
### Added
|
|
976
|
+
|
|
977
|
+
- Event-driven architecture for extensibility and observability
|
|
978
|
+
|
|
979
|
+
## [0.1.6] - 2025-09-01
|
|
980
|
+
|
|
981
|
+
### Added
|
|
982
|
+
|
|
983
|
+
- Inertia.js support for React/Vue/Svelte frontends
|
|
984
|
+
|
|
985
|
+
## [0.1.5] - 2025-08-15
|
|
986
|
+
|
|
987
|
+
### Added
|
|
988
|
+
|
|
989
|
+
- Apple Sign In integration
|
|
990
|
+
- Social login callback support
|
|
991
|
+
- Server-side authorization code flow for mobile
|
|
992
|
+
|
|
993
|
+
### Fixed
|
|
994
|
+
|
|
995
|
+
- Social callback no longer always required
|
|
996
|
+
|
|
997
|
+
## [0.1.4] - 2025-08-01
|
|
998
|
+
|
|
999
|
+
### Added
|
|
1000
|
+
|
|
1001
|
+
- Google OAuth integration
|
|
1002
|
+
- Configurable custom scopes and claims
|
|
1003
|
+
|
|
1004
|
+
## [0.1.3] - 2025-07-15
|
|
1005
|
+
|
|
1006
|
+
### Added
|
|
1007
|
+
|
|
1008
|
+
- JWT scope validation in API authentication
|
|
1009
|
+
- Configurable OAuth token expiration
|
|
1010
|
+
|
|
1011
|
+
## [0.1.2] - 2025-07-01
|
|
1012
|
+
|
|
1013
|
+
### Fixed
|
|
1014
|
+
|
|
1015
|
+
- Client credential flow bugs
|
|
1016
|
+
|
|
1017
|
+
## [0.1.1] - 2025-06-15
|
|
1018
|
+
|
|
1019
|
+
### Changed
|
|
1020
|
+
|
|
1021
|
+
- Initial version bump after core setup
|
|
1022
|
+
|
|
1023
|
+
## [0.1.0] - 2025-06-01
|
|
1024
|
+
|
|
1025
|
+
### Added
|
|
1026
|
+
|
|
1027
|
+
- Core authentication engine with web and API dual-mount architecture
|
|
1028
|
+
- Cookie-based web sessions with CSRF protection
|
|
1029
|
+
- JWT-based API authentication
|
|
1030
|
+
- OAuth 2.0 authorization code flow with PKCE support
|
|
1031
|
+
- Implicit, client credentials, and password grant flows
|
|
1032
|
+
- Refresh token flow
|
|
1033
|
+
- Passwordless authentication via email/SMS OTP
|
|
1034
|
+
- STI-based session management (Browser, Device, Service)
|
|
1035
|
+
- STI-based identifiers (Email, Phone, Username)
|
|
1036
|
+
- Client application management with secret rotation
|
|
1037
|
+
- Configuration system with schema DSL
|
|
1038
|
+
- Install generator
|