standard_singpass 0.3.0 → 0.3.1

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: 1ecc46cf176551f25806d49f04c64ba204801c137b46c7c5970fd6a0f97d650a
4
- data.tar.gz: f5d5c709779c0082d75c4909d549ccf451daa5406900ac7e05f83e23d2e69994
3
+ metadata.gz: dbda49a3c31fd6844db0d58c66c0bf1c8654882d37105c58dd1537a2e2cb13c5
4
+ data.tar.gz: 5b4ebd78e72bab8b36a2d8d5c298cc8f99e277c018b23a5b42ff7dfb4030292e
5
5
  SHA512:
6
- metadata.gz: a30193c9f2d2a7633f3833e1eb186df1dd09b7fa922de35ab0c7dd2d1b7acb5f8c867e91e14e86fb82cce0f814f047ee1d402911334100d1fde4ac1b353b1f05
7
- data.tar.gz: 886ac97c7a2ea5125cc079ef141dcb6c303d0ac02e0c48343a6e20aff20ed889d66af105305f315e5aab5f69ed1aaaa7a781467482326162511a0690780fd812
6
+ metadata.gz: 47d70dda7f2948b55b59afac864885452e776aefe9e8806a16e62f31b9ca02bc6a8772e5eff7849b25ca3a8805487d45ea3c63a77412a713ba3c7b11f6e7f3e1
7
+ data.tar.gz: cd7b80ae5bea3214b3d1bb146801c673fe2934b8b245865ef7bd61dee8952a0d2f5dd670f24c08e925fb083fb6e8debe1901ad2ce9fcee57b8fe9174cff503af
data/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.1] - 2026-09-24
11
+
12
+ ### Fixed
13
+
14
+ - **`minimum_acr` is no longer sent to Singpass as `acr_values` on the Pushed Authorization Request.** Singpass rejects that parameter for MyInfo — `PAR failed (HTTP 400) invalid_request — "The acr_values parameter can only be used for specific use cases."` — so any host that set `minimum_acr` (as the README and the install generator recommended, via `MYINFO_MIN_ACR`) failed every MyInfo login at the first step. This took down production onboarding for a consuming app for roughly four days before it was traced. `minimum_acr` is now purely a client-side check: the id_token `acr` claim is still validated against it (`AuthenticationError` when below the floor or missing; `ConfigurationError` for an unrecognised URN), and the PAR body never carries `acr_values`.
15
+
16
+ **Behaviour change / upgrade note:** hosts with `minimum_acr` set go from "every PAR fails with HTTP 400" to "PAR succeeds and the returned id_token `acr` is checked locally". Hosts that left it unset see no change. If you had pinned `minimum_acr = nil` as a workaround, you can keep it — MyInfo's assurance level is governed by Singpass server-side and the floor is optional. If you wired it from `MYINFO_MIN_ACR`, consider removing that env wiring (see Documentation below).
17
+
18
+ ### Documentation
19
+
20
+ - **Install generator no longer wires `c.minimum_acr = ENV["MYINFO_MIN_ACR"]`** or documents `MYINFO_MIN_ACR`. The option is left commented out with a warning that it is a client-side check only and that a wrong value fails every login closed.
21
+ - **README:** Installation now mentions `bin/rails g standard_singpass:install`; the `MYINFO_MIN_ACR` wiring is removed from the configuration example; a new "Assurance level (`minimum_acr`)" section explains what the option does and does not do; and the default-scope comment now gives the real size of `DEFAULT_SCOPE` (42 scopes — `openid` plus 41 attributes — not "a 36-attribute set").
22
+ - **Consumer list in `CLAUDE.md` narrowed to the one app that actually consumes this gem.** It said "the rarebit-one workspace's web apps", which reads as all five and would send a rollout at four apps with no Singpass integration. The single consumer is `fundbright-web`, in the sibling `~/Workspace/fundbright/` workspace. The list is now bulleted so the new advisory `check-gem-family-drift.sh` can diff it against the canonical matrix in the workspace's `rollout-gem/SKILL.md` — prose that a human reads as one thing and a script as another is how this drifted in the first place.
23
+
10
24
  ## [0.3.0] - 2026-07-30
11
25
 
12
26
  ### Added
data/README.md CHANGED
@@ -12,6 +12,15 @@ Add to your Gemfile:
12
12
  gem "standard_singpass"
13
13
  ```
14
14
 
15
+ Then generate the initializer:
16
+
17
+ ```bash
18
+ bin/rails g standard_singpass:install
19
+ ```
20
+
21
+ This writes `config/initializers/standard_singpass.rb`, mapping the `MYINFO_*`
22
+ environment variables onto the gem's configuration (see below).
23
+
15
24
  ## Configuration
16
25
 
17
26
  ```ruby
@@ -24,17 +33,18 @@ StandardSingpass::Myinfo.configure do |c|
24
33
  c.client_id = ENV["MYINFO_CLIENT_ID"]
25
34
  c.redirect_url = ENV["MYINFO_REDIRECT_URL"]
26
35
 
27
- # Optional: override default scope (defaults to a 36-attribute set covering
28
- # identity, contact, income, employment, housing, assets, vehicles).
36
+ # Optional: override default scope (defaults to 42 scopes `openid` plus 41
37
+ # MyInfo attributes covering identity, contact, income, employment, housing,
38
+ # assets, vehicles).
29
39
  # c.scope = "openid name email ..."
30
40
 
31
41
  # Required: full private JWKS JSON containing both sig (ES256) and enc
32
42
  # (ECDH-ES+A256KW) keys with the private scalar `d`.
33
43
  c.private_jwks_json = ENV["MYINFO_PRIVATE_JWKS"]
34
44
 
35
- # Optional: enforce minimum Authentication Context Class Reference. Set to
36
- # e.g. "urn:singpass:authentication:loa:3" to require high-assurance.
37
- c.minimum_acr = ENV["MYINFO_MIN_ACR"]
45
+ # Optional: client-side floor on the id_token `acr` claim. Never sent to
46
+ # Singpass — see "Assurance level (`minimum_acr`)" below before setting it.
47
+ # c.minimum_acr = "urn:singpass:authentication:loa:2"
38
48
 
39
49
  # Optional: wrap outbound HTTP calls with a circuit breaker / retry layer.
40
50
  # Defaults to identity (no wrapper).
@@ -54,6 +64,33 @@ StandardSingpass::Myinfo.configure do |c|
54
64
  end
55
65
  ```
56
66
 
67
+ ## Assurance level (`minimum_acr`)
68
+
69
+ `config.minimum_acr` is an optional, **client-side** check. When set to a
70
+ Singpass LOA URN (`urn:singpass:authentication:loa:2` or `...:loa:3`), the
71
+ client rejects any id_token whose `acr` claim is below that level — or missing
72
+ — with `StandardSingpass::Myinfo::AuthenticationError`. A value that is not a
73
+ recognised LOA URN raises `ConfigurationError`. Unset or blank disables the
74
+ check.
75
+
76
+ It is **never sent to Singpass.** Singpass rejects `acr_values` on a MyInfo
77
+ Pushed Authorization Request:
78
+
79
+ ```
80
+ PAR failed (HTTP 400) invalid_request —
81
+ "The acr_values parameter can only be used for specific use cases."
82
+ ```
83
+
84
+ Versions up to 0.3.0 forwarded a configured `minimum_acr` as `acr_values`, so
85
+ setting it broke every MyInfo login. From 0.3.1 the PAR request never carries
86
+ `acr_values`. MyInfo's assurance level is governed by Singpass server-side, so
87
+ most hosts should leave `minimum_acr` unset; if you do set it, set it in code
88
+ rather than from a loosely-managed environment variable, because a wrong value
89
+ fails every login closed.
90
+
91
+ The `acr` Singpass actually returned is surfaced as `result[:id_token_acr]`
92
+ from `get_person_data` for audit, whether or not a floor is configured.
93
+
57
94
  ## Mock mode
58
95
 
59
96
  `config.mock_mode` makes the host serve fixture persona data
@@ -16,8 +16,6 @@
16
16
  # MYINFO_SCOPE Space-separated scopes (defaults to DEFAULT_SCOPE)
17
17
  # MYINFO_USERINFO_URL Userinfo endpoint override (rarely needed)
18
18
  # MYINFO_USERINFO_JWKS_URL Userinfo JWKS endpoint override (rarely needed)
19
- # MYINFO_MIN_ACR Required Authentication Context Class Reference
20
- # URN, e.g. urn:singpass:authentication:loa:3
21
19
  # MYINFO_MOCK_MODE When set, suppresses missing-key warnings and
22
20
  # unlocks any mock-callback routes the host
23
21
  # registers. The gem REFUSES TO BOOT if this is set
@@ -40,8 +38,17 @@ StandardSingpass::Myinfo.configure do |c|
40
38
  c.userinfo_url = ENV["MYINFO_USERINFO_URL"] if ENV["MYINFO_USERINFO_URL"].present?
41
39
  c.userinfo_jwks_url = ENV["MYINFO_USERINFO_JWKS_URL"] if ENV["MYINFO_USERINFO_JWKS_URL"].present?
42
40
 
43
- c.minimum_acr = ENV["MYINFO_MIN_ACR"]
44
- c.mock_mode = ENV["MYINFO_MOCK_MODE"].present?
41
+ c.mock_mode = ENV["MYINFO_MOCK_MODE"].present?
42
+
43
+ # Optional client-side floor on the id_token `acr` claim. The gem rejects an
44
+ # id_token whose assurance level is below it (AuthenticationError); it is
45
+ # NEVER sent to Singpass — `acr_values` on a MyInfo PAR is rejected with
46
+ # HTTP 400 invalid_request. MyInfo's assurance level is governed by Singpass
47
+ # server-side, so most hosts should leave this unset.
48
+ #
49
+ # WARNING: do not wire this to an environment variable without a test that
50
+ # exercises the real flow — a stray value fails every login closed.
51
+ # c.minimum_acr = "urn:singpass:authentication:loa:2"
45
52
 
46
53
  # How the gem decides whether this deploy is real production, for the
47
54
  # mock-mode boot guard. Defaults to Rails.env.production?. Staging and
@@ -60,17 +60,17 @@ module StandardSingpass
60
60
  )
61
61
  }
62
62
 
63
- # Ask Singpass to enforce a minimum assurance level upstream. The same
64
- # config attribute also drives downstream validation of the returned
65
- # id_token (validate_id_token_acr) — defense in depth. When unset, we
66
- # skip both the request parameter and the validator entirely; useful
67
- # for sandbox personas that may return non-conformant acr values.
68
- # Concrete URN per Singpass: `urn:singpass:authentication:loa:N` (N
69
- # is 2 or 3; Singpass never issues below LOA 2). `.to_s.strip` mirrors
70
- # the validator so whitespace-only values are treated as unset and any
71
- # value sent over the wire is trimmed.
72
- min_acr = @minimum_acr.to_s.strip
73
- body[:acr_values] = min_acr unless min_acr.empty?
63
+ # `acr_values` is deliberately NEVER sent, even when `minimum_acr` is
64
+ # configured. Singpass rejects it on a MyInfo PAR:
65
+ #
66
+ # PAR failed (HTTP 400) invalid_request
67
+ # "The acr_values parameter can only be used for specific use cases."
68
+ #
69
+ # Until 0.3.1 a non-empty `minimum_acr` was forwarded here, which broke
70
+ # every MyInfo onboarding for any host that set it. `minimum_acr` is
71
+ # now purely a client-side check of the returned id_token `acr` claim
72
+ # (see validate_id_token_acr); MyInfo's assurance level itself is
73
+ # governed by Singpass server-side.
74
74
 
75
75
  with_network_wrapper do
76
76
  response = http_connection.post(@par_url) do |req|
@@ -299,8 +299,9 @@ module StandardSingpass
299
299
  # Enforce a minimum Authentication Context Class Reference (`acr`) on the
300
300
  # id_token. The floor is configured via `minimum_acr` so staging and
301
301
  # production can diverge — staging may tolerate looser values returned by
302
- # MyInfo sandbox personas. When the attr is unset or blank, both this
303
- # validator and the upstream PAR `acr_values` parameter are skipped.
302
+ # MyInfo sandbox personas. When the attr is unset or blank, this
303
+ # validator is skipped. This is the ONLY effect of `minimum_acr` it is
304
+ # never sent to Singpass as `acr_values` (see push_authorization_request).
304
305
  #
305
306
  # Singpass's `acr` URN format is `urn:singpass:authentication:loa:N`
306
307
  # where N is 2 or 3 (no LOA 1 path — Singpass's IdP is 2FA by design).
@@ -14,7 +14,11 @@
14
14
  #
15
15
  # Optional attributes:
16
16
  # c.scope - Space-separated scopes (defaults to DEFAULT_SCOPE)
17
- # c.minimum_acr - Required Authentication Context Class Reference URN
17
+ # c.minimum_acr - Minimum Authentication Context Class Reference URN
18
+ # (e.g. urn:singpass:authentication:loa:3) checked
19
+ # client-side against the id_token `acr` claim.
20
+ # Never sent to Singpass: `acr_values` on a MyInfo
21
+ # PAR is rejected with HTTP 400 invalid_request.
18
22
  # c.network_wrapper - Lambda wrapping outbound Faraday calls (e.g. circuit breaker)
19
23
  # c.mock_mode - When true, suppresses missing-key warnings
20
24
  # (and is refused outright on a production deploy
@@ -1,3 +1,3 @@
1
1
  module StandardSingpass
2
- VERSION = "0.3.0"
2
+ VERSION = "0.3.1"
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.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim