standard_id 0.55.1 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79b115262b448690a7d1429716da8b36e6422be79e9406868684a3744e0cf3e8
4
- data.tar.gz: a43dc5ed9ea2ff5bd5317809f79c9326d1df14a2115f63e631090a7fe15042cd
3
+ metadata.gz: 5d21b27586542e7f6f9bc312ace8fed7a75a8b75c4633e470232890b869f802f
4
+ data.tar.gz: 46b0df7981fa6785cdbc86a0ea65e52bf30c6c293a6198671d43169dd82fb955
5
5
  SHA512:
6
- metadata.gz: a184a92b4a8f865db6e4fd4f05cbaa9e44d1156407ed61d30530ee6379269cc9cc3da0d9f64b800c7fc44461abc28d2df44ebe2d4562a6dbde96d0f69a106ec7
7
- data.tar.gz: abc3fe5d187c94408406cfeba9e27a699f5542b30a1979c268c7b5fe7d2986ea29b56dcb6d4b8399b8e0df3a7ed56271951a49eec9a25076d5cd568a1a30b9c5
6
+ metadata.gz: c54196984851c658850c910a1509ffbc4231911759a0f003d9f3915002459ca5d313a61643abdd43edeac85d332ef8bc1c4173d65e9ca3fe2f747f44aff93e14
7
+ data.tar.gz: 44510185f41ecd77b81c1d44342e5e6b2e432c75232c91cd3347ddea25fa3115d370e05d0af2d54a878a2dbf38cc5888bd0bdb82de12d9d1888b9df60474c10a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.56.0] - 2026-10-26
11
+
12
+ ### Added
13
+
14
+ - **`c.events.log_login_identifiers` (default `false`).** `LoggingSubscriber` now redacts the submitted
15
+ email/login in `AUTHENTICATION_ATTEMPT_STARTED` (debug) and `AUTHENTICATION_FAILED` (warn) log lines by
16
+ default: email-shaped values are logged as `lo***@domain`; everything else as `lo***`. Set
17
+ `c.events.log_login_identifiers = true` to restore the previous behaviour (full value logged) for apps
18
+ that need it for debugging.
19
+
20
+ - **Boot-time warning when `verify_issuer: false` is combined with a configured `issuer`.** The existing
21
+ warning fires when `issuer` is blank; this companion fires when `issuer` is set but `verify_issuer` is
22
+ explicitly `false`, reminding operators it is a migration flag to remove once all in-flight tokens have
23
+ expired.
24
+
25
+ ### Fixed
26
+
27
+ - **Inertia history is now cleared on sign-out** for hosts that set `use_inertia = true`.
28
+ `Web::LogoutController#create` and the current-session branch of
29
+ `Web::SessionsController#destroy` now pass `inertia: { clear_history: true }` on
30
+ the redirect, which stores a session flag that the next Inertia render consumes
31
+ (`clearHistory: true` in the page JSON). Without this, the old history-encryption
32
+ key remained in `sessionStorage` and `Back` could still decrypt pages viewed while
33
+ signed in. Hosts without `use_inertia` are unaffected.
34
+
35
+ Rolling this out requires a release followed by a `/rollout-gem` to all five
36
+ consumers (fundbright-web, nutripod-web, jumpdrive-web, luminality-web,
37
+ sidekick-web).
38
+
10
39
  ## [0.55.1] - 2026-10-23
11
40
 
12
41
  ### Fixed
@@ -14,7 +14,8 @@ module StandardId
14
14
 
15
15
  def create
16
16
  revoke_current_session!
17
- redirect_to logout_destination, notice: "Successfully signed out"
17
+ redirect_to logout_destination, notice: "Successfully signed out",
18
+ **(use_inertia? ? { inertia: { clear_history: true } } : {})
18
19
  end
19
20
 
20
21
  private
@@ -15,7 +15,8 @@ module StandardId
15
15
  if session == current_session
16
16
  # If revoking current session, sign out and redirect
17
17
  revoke_current_session!
18
- redirect_to "/", notice: "Session revoked. You have been signed out."
18
+ redirect_to "/", notice: "Session revoked. You have been signed out.",
19
+ **(use_inertia? ? { inertia: { clear_history: true } } : {})
19
20
  else
20
21
  # Revoke other session
21
22
  session.revoke!
@@ -153,6 +153,20 @@ StandardId::ConfigSchema.define do
153
153
  scope :events do
154
154
  field :enable_logging, type: :boolean, default: false
155
155
 
156
+ # Whether to log the full login identifier (email / username) in
157
+ # `AUTHENTICATION_ATTEMPT_STARTED` and `AUTHENTICATION_FAILED` log lines.
158
+ #
159
+ # Defaults to `false`: a redacted form is logged instead
160
+ # (`local-part[0..1]***@domain` for email-shaped values, first two
161
+ # characters + *** for everything else). This prevents email addresses from
162
+ # accumulating in log sinks (Sentry, Datadog, etc.) on every failed login
163
+ # attempt, including attacker probes.
164
+ #
165
+ # Set to `true` only for apps that have already accepted the privacy
166
+ # trade-off and need the full value for debugging — typically behind an
167
+ # environment flag that is off in production.
168
+ field :log_login_identifiers, type: :boolean, default: false
169
+
156
170
  # Publish `<key>_id` / `<key>_gid` alongside every record-valued payload key
157
171
  # (rarebit-one/rarebit-ops#296).
158
172
  #
@@ -81,10 +81,7 @@ module StandardId
81
81
  StandardId::Events::Subscribers::PasswordlessDeliverySubscriber.attach
82
82
  StandardId::Events::Subscribers::PasswordResetDeliverySubscriber.attach
83
83
 
84
- if StandardId.config.issuer.blank?
85
- Rails.logger.warn("[StandardId] No issuer configured. JWT tokens will not include or verify the 'iss' claim. " \
86
- "Set StandardId.config.issuer in your initializer for production use.")
87
- end
84
+ StandardId::Engine.warn_about_issuer_config!
88
85
 
89
86
  # Fail loudly if `association_strict_loading` was assigned after the
90
87
  # host's Account class body already ran — that assignment has no effect,
@@ -175,5 +172,21 @@ module StandardId
175
172
  "audiences (e.g., ['web', 'api']) to close cross-audience replay vectors."
176
173
  )
177
174
  end
175
+
176
+ # Warn when the issuer configuration is missing or when `verify_issuer`
177
+ # is explicitly `false` with an issuer set (migration flag left behind).
178
+ # Extracted to a module method so specs can exercise it without booting
179
+ # a second Rails app.
180
+ def self.warn_about_issuer_config!
181
+ if StandardId.config.issuer.blank?
182
+ Rails.logger.warn("[StandardId] No issuer configured. JWT tokens will not include or verify the 'iss' claim. " \
183
+ "Set StandardId.config.issuer in your initializer for production use.")
184
+ elsif StandardId.config.verify_issuer == false
185
+ Rails.logger.warn("[StandardId] verify_issuer is explicitly false but an issuer is configured " \
186
+ "(#{StandardId.config.issuer.inspect}). Tokens will include the 'iss' claim but " \
187
+ "incoming tokens are NOT required to carry it. This is a migration flag — remove " \
188
+ "verify_issuer: false from your initializer once all in-flight tokens have expired.")
189
+ end
190
+ end
178
191
  end
179
192
  end
@@ -110,7 +110,10 @@ module StandardId
110
110
  payload[:account_id] = event[:account_id]
111
111
  end
112
112
 
113
- payload[:login] = event[:account_lookup] || event[:login] || event[:username] if event[:account_lookup] || event[:login] || event[:username]
113
+ raw_login = event[:account_lookup] || event[:login] || event[:username]
114
+ if raw_login
115
+ payload[:login] = log_login_identifiers? ? raw_login : redact_login(raw_login)
116
+ end
114
117
  payload[:auth_method] = event[:auth_method] if event[:auth_method]
115
118
  payload[:grant_type] = event[:grant_type] if event[:grant_type]
116
119
  payload[:provider] = event[:provider] if event[:provider]
@@ -120,6 +123,40 @@ module StandardId
120
123
 
121
124
  payload
122
125
  end
126
+
127
+ def log_login_identifiers?
128
+ config = StandardId.config
129
+ return false unless config.respond_to?(:events)
130
+
131
+ config.events.log_login_identifiers
132
+ end
133
+
134
+ # Redact a login identifier for logging.
135
+ #
136
+ # Email-shaped values (`local@domain`): keep the first two characters of
137
+ # the local part, mask the rest as `***`, keep the domain verbatim.
138
+ # Everything else: keep the first two characters, mask the rest as `***`.
139
+ #
140
+ # The local part's length is not exposed (all masks are `***`), and the
141
+ # domain is kept so operators can still tell at a glance which service
142
+ # the attempt targeted (e.g. gmail.com vs corporate SSO).
143
+ def redact_login(value)
144
+ str = value.to_s
145
+ # Split on the FIRST '@' only (limit 2) to detect email shape without
146
+ # a regex. A regex such as /\A(.+)@(.+)\z/ is polynomial on strings
147
+ # like "a@a@a@…" because the two greedy `.+` quantifiers can match the
148
+ # same characters in exponentially many ways — a ReDoS vector on
149
+ # attacker-controlled login values. String#split is linear.
150
+ parts = str.split("@", 2)
151
+ if parts.length == 2
152
+ local, domain = parts
153
+ prefix = local.length > 2 ? local[0, 2] : local[0, 1]
154
+ "#{prefix}***@#{domain}"
155
+ else
156
+ prefix = str.length > 2 ? str[0, 2] : str[0, 1]
157
+ "#{prefix}***"
158
+ end
159
+ end
123
160
  end
124
161
  end
125
162
  end
@@ -1,3 +1,3 @@
1
1
  module StandardId
2
- VERSION = "0.55.1"
2
+ VERSION = "0.56.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard_id
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.55.1
4
+ version: 0.56.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim