standard_id 0.24.0 → 0.26.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: 96bb41b62e41b340083b98a19c91913d8a1342a1895150d68595cba6a576d039
4
- data.tar.gz: b85e690728fad61be9a1ab5fa047096cf3db2b97bea9b9403774cd53895216f6
3
+ metadata.gz: a0d8132ce80b99059528d033a05dc3d6ec4c421d6eaea14725e51357639f4808
4
+ data.tar.gz: 2c8386bc4ea25b23e54553ed0ad3ba5816fc47efcb64003aa02252e0af54e54d
5
5
  SHA512:
6
- metadata.gz: 4bebc333835970bebc067517014364dce32cfa6f3d9165a64aa7b82f3464f5862439c9fd7e40d73ae6d79a478490abe749609804c168d9ae8669d29e7d6d861e
7
- data.tar.gz: 23a59d0a655a3d933ed953923ce5f2c4f63240543b06f094faa182366c6d147e27b769c6c6230821d0a1c60501399ac7569296840255403b95953faf37a5ba8b
6
+ metadata.gz: e69d8a20c4419deec38b8cc3c69b51a6b99b6a6c2a9043bf0a16625bb7539faf8ebf9095856d2fcad9a6ed2eda011863ba374d7872d681dc229e6a682d273e99
7
+ data.tar.gz: ed39942791e51fff0b333d9026e4707a52c8acdfffc5a40c447939b5f486d971597d3711417453bb1ecb66f736b78af607bf8bb9607c0a9ae1594139d3d4a7ff
data/README.md CHANGED
@@ -450,7 +450,17 @@ end
450
450
 
451
451
  **Realm isolation.** `realm:` is a free-form string that partitions challenges by purpose. A code issued for realm `"widget_contact_verification"` cannot be used to verify against realm `"authentication"` (or any other realm) — even for the same `target`. Choose a stable string per flow.
452
452
 
453
- **Bypass code (E2E testing).** When `StandardId.config.passwordless.bypass_code` is set (and `Rails.env` is not `"production"`), `Otp.verify` accepts the bypass code for **any realm**. This replaces per-app bypass ENV checks and works consistently across `Otp.verify` and the built-in passwordless login flow. Never set `bypass_code` in production — it will raise if you try.
453
+ **Bypass code (E2E testing).** When `StandardId.config.passwordless.bypass_code` is set (and the deploy is not production), `Otp.verify` accepts the bypass code for **any realm**. This replaces per-app bypass ENV checks and works consistently across `Otp.verify` and the built-in passwordless login flow. Never set `bypass_code` in production — it will raise if you try.
454
+
455
+ By default "production" means `Rails.env.production?`. Apps that distinguish a physical deploy environment from `RAILS_ENV` (e.g. a staging box that still runs `RAILS_ENV=production`) can override the decision with a callable:
456
+
457
+ ```ruby
458
+ # Allow the bypass code on a physically-staging box, still refused on real prod.
459
+ c.passwordless.production_env_detector = -> { AppEnv.production? }
460
+ c.passwordless.bypass_code = ENV["STANDARD_ID_BYPASS_CODE"] unless AppEnv.production?
461
+ ```
462
+
463
+ `production_env_detector` takes no arguments and returns a boolean; when `nil` (the default) the gem falls back to `Rails.env.production?`, so existing consumers are unaffected.
454
464
 
455
465
  **Back-compat.** The existing passwordless authentication flow continues to work unchanged. `Otp.issue`/`Otp.verify` are a new addition — you can migrate direct `CodeChallenge.create!` calls at your own pace.
456
466
 
@@ -102,13 +102,27 @@ StandardId::ConfigSchema.define do
102
102
  field :max_attempts_per_challenge, type: :integer, default: nil
103
103
 
104
104
  field :retry_delay, type: :integer, default: 30 # 30 seconds
105
- # Bypass code for E2E testing — NEVER set in production (raises).
106
- # When set and Rails.env != "production", this code is accepted by
105
+ # Bypass code for E2E testing — refused on production deploys (raises).
106
+ # When set and the deploy is *not* production, this code is accepted by
107
107
  # both the built-in passwordless login and by StandardId::Otp.verify
108
108
  # for *any* realm. Use a long, non-guessable value and unset it
109
109
  # outside test environments.
110
+ #
111
+ # "Production" is decided by production_env_detector below (default
112
+ # Rails.env.production?), so host apps that distinguish a physical deploy
113
+ # environment from RAILS_ENV can still gate this correctly.
110
114
  field :bypass_code, type: :string, default: nil
111
115
 
116
+ # Optional callable deciding whether the current deploy is "production"
117
+ # for the purpose of the bypass-code guard. Takes no args, returns a
118
+ # boolean. When nil (default), falls back to Rails.env.production? — so
119
+ # existing consumers are unchanged. Host apps that distinguish a physical
120
+ # deploy environment from RAILS_ENV (e.g. APP_ENVIRONMENT, which stays
121
+ # RAILS_ENV=production on a physically-staging box) can supply
122
+ # `-> { AppEnv.production? }` to allow a bypass code on staging while it
123
+ # stays refused on real production.
124
+ field :production_env_detector, type: :any, default: nil
125
+
112
126
  # Custom username validator for passwordless flows.
113
127
  # When set, called before OTP generation to validate the recipient address.
114
128
  # Must be a callable (lambda/proc) that receives (username, connection_type)
@@ -204,7 +204,7 @@ module StandardId
204
204
  bypass_code = StandardId.config.passwordless.bypass_code
205
205
  return unless bypass_code.present?
206
206
 
207
- if defined?(Rails) && Rails.env.production?
207
+ if bypass_production_deploy?
208
208
  raise "STANDARD_ID_BYPASS_CODE must not be set in production"
209
209
  end
210
210
 
@@ -246,6 +246,17 @@ module StandardId
246
246
  end
247
247
  end
248
248
 
249
+ # Whether the bypass code is forbidden because this is a production
250
+ # deploy. Defers to config.passwordless.production_env_detector when set
251
+ # (host apps that distinguish a physical deploy env from RAILS_ENV), else
252
+ # falls back to Rails.env.production?.
253
+ def bypass_production_deploy?
254
+ detector = StandardId.config.passwordless.production_env_detector
255
+ return !!detector.call if detector
256
+
257
+ defined?(Rails) && Rails.env.production?
258
+ end
259
+
249
260
  def resolve_target_and_channel!(email, phone)
250
261
  if email.present?
251
262
  @target = email.to_s.strip
@@ -1,3 +1,3 @@
1
1
  module StandardId
2
- VERSION = "0.24.0"
2
+ VERSION = "0.26.0"
3
3
  end
@@ -35,7 +35,7 @@ module StandardId
35
35
  # Store in both session and encrypted cookie for backward compatibility
36
36
  # Action Cable will use the encrypted cookie
37
37
  session[:session_token] = browser_session.token
38
- cookies.encrypted[:session_token] = browser_session.token
38
+ write_session_cookie(browser_session)
39
39
  if scope_name
40
40
  scopes = Array(session[:standard_id_scopes])
41
41
  scopes << scope_name.to_s unless scopes.include?(scope_name.to_s)
@@ -71,6 +71,22 @@ module StandardId
71
71
 
72
72
  private
73
73
 
74
+ # Persist the session token in an encrypted cookie whose lifetime matches
75
+ # the DB session's expires_at, so an authenticated session survives a full
76
+ # browser restart (a bare session cookie would be cleared on browser close,
77
+ # logging the user out well before the BrowserSession actually expires).
78
+ # httponly/secure/same_site harden the cookie; httponly does not affect
79
+ # Action Cable, which reads the cookie server-side.
80
+ def write_session_cookie(browser_session)
81
+ cookies.encrypted[:session_token] = {
82
+ value: browser_session.token,
83
+ expires: browser_session.expires_at,
84
+ httponly: true,
85
+ secure: request.ssl?,
86
+ same_site: :lax
87
+ }
88
+ end
89
+
74
90
  def load_current_account
75
91
  if StandardId.config.account_scope
76
92
  account_id = current_session&.account_id
@@ -121,7 +137,7 @@ module StandardId
121
137
  token_manager.create_browser_session(password_credential.account, remember_me: true).tap do |browser_session|
122
138
  # Store in both session and encrypted cookie for backward compatibility
123
139
  session[:session_token] = browser_session.token
124
- cookies.encrypted[:session_token] = browser_session.token
140
+ write_session_cookie(browser_session)
125
141
  cookies[:remember_token] = token_manager.create_remember_token(password_credential)
126
142
  end
127
143
  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.24.0
4
+ version: 0.26.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim