standard_singpass 0.5.0 → 0.7.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: 6a27b57ad2ae40ae92c93b8328f2fe0849d22bc95ecc1812bb1f6899f2eb36e1
4
- data.tar.gz: 3d8c189d90e1cd51db83b73f7bee5fe78d4f856b90721707a12befd8af8c37e6
3
+ metadata.gz: 35e2fb1210199ace98e5db77de85e6a3650ee9297bffd8f8a0f2d16f743cba8e
4
+ data.tar.gz: 594ff25cfe621eb103dc397889b91a21cb49316b23ba6b2e849d42f5046a5a33
5
5
  SHA512:
6
- metadata.gz: eeca8fd0904977618487859b6aa207f4cd3394aa016943f1f6e177ee71404f9e95bdef1b68fb2a68b47d9463f5779871f5b0d8257f834fd403d954c81537816b
7
- data.tar.gz: a321882ef3e389555bb2427262f863961c38dd30e2a265959cbc383bcad05de9f63b243b248115f40ca5adc5fd267f3f6a2a860a36a6d8e8cbe92c5bd6369b1f
6
+ metadata.gz: '008b3123acb642f298caf073931dfd5778645dea026cdd3baf99045d4cb46caa74834d77cee80861042dee89d8a454d58717f39c20dc3bd2746c319716157726'
7
+ data.tar.gz: d93185d6bb749e2413d8181f35b6383863fa95afe78642c4d6548b92bcf7f441fc8d9a0ba0a440e6f7c954883ffc4736b2684025e68b6cf4dceaae37c65882ac
data/CHANGELOG.md CHANGED
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.0] - 2026-10-25
11
+
12
+ ### Changed
13
+
14
+ - **`get_person_data` now requires a `nonce` by default.** Passing `nonce: nil` (or omitting it) raises
15
+ `StandardSingpass::Myinfo::ConfigurationError` with a clear message. The nonce binds the id_token to
16
+ the session and prevents replay attacks; PKCE + DPoP substantially raise the bar but do not fully close
17
+ the replay window without it. The only consumer, `fundbright-web`, already passes the nonce from the
18
+ session on every call, so it is unaffected. To opt out for flows where no nonce was included in the PAR
19
+ request, pass `skip_nonce_validation: true` — this is intentionally verbose so it stands out in review.
20
+
21
+ ## [0.6.0] - 2026-10-22
22
+
23
+ ### Changed
24
+
25
+ - **The mock-mode guard follows `APP_ENVIRONMENT`, failing closed.** Without a
26
+ `production_env_detector`, "production deploy" is now `APP_ENVIRONMENT` when it
27
+ is set (only `staging`, `preview`, `development` and `test` are non-production;
28
+ any other value is production), else `Rails.env.production?` as before. A
29
+ staging box with `RAILS_ENV=production` and `APP_ENVIRONMENT=staging` boots
30
+ with mock mode on (reported as production-like) without detector wiring. The
31
+ same convention as standard_id, standard_audit and standard_health. A
32
+ configured detector still wins, so existing hosts are unaffected.
33
+ - **Requires Rails 8.1** (`rails >= 8.1, < 9`, was `>= 8.0`). Every consumer
34
+ app runs 8.1; 8.0 was never exercised in CI.
35
+
10
36
  ## [0.5.0] - 2026-09-24
11
37
 
12
38
  **Breaking.** Removes everything 0.4.0 deprecated. No other behaviour changes.
data/README.md CHANGED
@@ -60,8 +60,9 @@ StandardSingpass::Myinfo.configure do |c|
60
60
  c.mock_mode = ENV["MYINFO_MOCK_MODE"].present?
61
61
 
62
62
  # Optional: how the gem decides whether this deploy is real production, for
63
- # the mock-mode guard. Defaults to Rails.env.production?.
64
- # c.production_env_detector = -> { AppEnv.production? }
63
+ # the mock-mode guard. Defaults to APP_ENVIRONMENT (failing closed), then
64
+ # Rails.env.production? — see "Mock mode" below.
65
+ # c.production_env_detector = -> { ... }
65
66
  end
66
67
  ```
67
68
 
@@ -115,21 +116,19 @@ opt-out — a guard you can forget to wire is a guard that isn't there.
115
116
  | Anything else | yes | Silent. |
116
117
  | Any | no | Silent. |
117
118
 
118
- By default "production" means `Rails.env.production?`. Staging and preview
119
- deploys routinely run `RAILS_ENV=production` while being entirely legitimate
120
- places to enable mock mode, so hosts that distinguish a physical deploy
121
- environment from `RAILS_ENV` should say so:
122
-
123
- ```ruby
124
- # Mock mode stays legal on a physically-staging box, still refused on real prod.
125
- c.production_env_detector = -> { AppEnv.production? }
126
- ```
127
-
128
- `production_env_detector` takes no arguments and returns a boolean; when `nil`
129
- (the default) the gem falls back to `Rails.env.production?`, so existing
130
- consumers are unaffected. The production-like tier reports through
131
- `Rails.error` rather than a specific error tracker, so whatever the host has
132
- subscribed (Sentry included) picks it up.
119
+ "Production" follows the estate's `APP_ENVIRONMENT` convention (the same rule
120
+ standard_id, standard_audit and standard_health use): when `APP_ENVIRONMENT` is
121
+ set, only `staging`, `preview`, `development` and `test` are non-production and
122
+ any other value (a typo included) is production, so a mis-set variable fails
123
+ closed; when it is unset, `Rails.env.production?` decides. Staging and preview
124
+ deploys routinely run `RAILS_ENV=production`, so with `APP_ENVIRONMENT=staging`
125
+ they land in the production-like tier, where mock mode boots and is reported,
126
+ with no extra wiring.
127
+
128
+ `config.production_env_detector` (a callable taking no arguments, returning a
129
+ boolean) overrides that, for a host with a different discriminator. The
130
+ production-like tier reports through `Rails.error` rather than a specific error
131
+ tracker, so whatever the host has subscribed (Sentry included) picks it up.
133
132
 
134
133
  ## Initiating the flow
135
134
 
@@ -101,10 +101,19 @@ module StandardSingpass
101
101
  "#{@authorize_url}?#{URI.encode_www_form(params)}"
102
102
  end
103
103
 
104
- sig { params(auth_code: String, code_verifier: String, dpop_key_pair: OpenSSL::PKey::EC, nonce: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }
105
- def get_person_data(auth_code:, code_verifier:, dpop_key_pair:, nonce: nil)
104
+ sig { params(auth_code: String, code_verifier: String, dpop_key_pair: OpenSSL::PKey::EC, nonce: T.nilable(String), skip_nonce_validation: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
105
+ def get_person_data(auth_code:, code_verifier:, dpop_key_pair:, nonce: nil, skip_nonce_validation: false)
106
+ unless skip_nonce_validation
107
+ if nonce.blank?
108
+ raise ConfigurationError,
109
+ "nonce is required for id_token replay protection. " \
110
+ "Pass the nonce you sent in the PAR request. " \
111
+ "Only pass skip_nonce_validation: true for flows where no nonce was sent in the PAR request."
112
+ end
113
+ end
114
+
106
115
  token_data = exchange_token(auth_code:, code_verifier:, dpop_key_pair:)
107
- id_token_payload = validate_id_token(token_data[:id_token], nonce:)
116
+ id_token_payload = validate_id_token(token_data[:id_token], nonce: skip_nonce_validation ? nil : nonce)
108
117
 
109
118
  person_data = fetch_userinfo(access_token: token_data[:access_token], dpop_key_pair:)
110
119
 
@@ -27,10 +27,13 @@ module StandardSingpass
27
27
  # loud-but-non-fatal signal instead.
28
28
  #
29
29
  # "Production deploy" is decided by `config.production_env_detector` when
30
- # set, falling back to `Rails.env.production?`. The gem cannot know about
31
- # a host's own environment discriminator (`APP_ENVIRONMENT` and friends),
32
- # and deliberately does not read ENV itself — the callable is the seam.
33
- # This mirrors `StandardId`'s `production_env_detector`.
30
+ # set, else by the estate's APP_ENVIRONMENT convention (shared with
31
+ # standard_id, standard_audit and standard_health): APP_ENVIRONMENT when it
32
+ # is set, where only staging, preview, development and test are
33
+ # non-production and any other value (a typo included) is production, so a
34
+ # mis-set variable fails closed; else `Rails.env.production?`. A staging box
35
+ # with RAILS_ENV=production and APP_ENVIRONMENT=staging is therefore
36
+ # production-*like*, not production, with no detector wiring.
34
37
  #
35
38
  # Wired automatically from the engine's `after_initialize`, so a host gets
36
39
  # the guard by configuring `mock_mode` at all. There is no opt-out: a
@@ -38,6 +41,9 @@ module StandardSingpass
38
41
  module MockModeGuard
39
42
  extend T::Sig
40
43
 
44
+ # APP_ENVIRONMENT values that are not production; anything else is.
45
+ NON_PRODUCTION_APP_ENVIRONMENTS = T.let(%w[staging preview development test].freeze, T::Array[String])
46
+
41
47
  sig { void }
42
48
  def self.check!
43
49
  return unless StandardSingpass::Myinfo.configuration.mock_mode
@@ -50,14 +56,16 @@ module StandardSingpass
50
56
  end
51
57
 
52
58
  # Whether this deploy is real production for the purpose of the guard.
53
- # Defers to config.production_env_detector when set (hosts that
54
- # distinguish a physical deploy environment from RAILS_ENV), else falls
55
- # back to Rails.env.production?.
59
+ # Defers to config.production_env_detector when set, else to
60
+ # APP_ENVIRONMENT (failing closed), else Rails.env.production?.
56
61
  sig { returns(T::Boolean) }
57
62
  def self.production_deploy?
58
63
  detector = StandardSingpass::Myinfo.configuration.production_env_detector
59
64
  return !!detector.call if detector
60
65
 
66
+ app_env = ENV["APP_ENVIRONMENT"].to_s.strip
67
+ return !NON_PRODUCTION_APP_ENVIRONMENTS.include?(app_env) unless app_env.empty?
68
+
61
69
  rails_production?
62
70
  end
63
71
 
@@ -75,7 +83,8 @@ module StandardSingpass
75
83
  "identity data that is indistinguishable from verified Myinfo " \
76
84
  "data downstream. The application refuses to boot. Unset the " \
77
85
  "mock-mode environment variable on this deploy and redeploy. If " \
78
- "this deploy is not actually production, set " \
86
+ "this deploy is not actually production, set APP_ENVIRONMENT " \
87
+ "(staging, preview, development or test) or " \
79
88
  "config.production_env_detector so the gem can tell."
80
89
  end
81
90
  private_class_method :production_message
@@ -1,3 +1,3 @@
1
1
  module StandardSingpass
2
- VERSION = "0.5.0"
2
+ VERSION = "0.7.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard_singpass
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim
@@ -15,7 +15,7 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '8.0'
18
+ version: '8.1'
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
21
  version: '9'
@@ -25,7 +25,7 @@ dependencies:
25
25
  requirements:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
- version: '8.0'
28
+ version: '8.1'
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
31
  version: '9'