standard_id 0.46.0 → 0.47.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 +12 -0
- data/README.md +18 -0
- data/app/controllers/standard_id/api/oauth/tokens_controller.rb +3 -1
- data/lib/standard_id/config/schema.rb +27 -0
- data/lib/standard_id/oauth/discovery_document.rb +3 -1
- data/lib/standard_id/oauth/grant_types.rb +18 -0
- data/lib/standard_id/oauth/password_flow.rb +17 -3
- data/lib/standard_id/version.rb +1 -1
- data/lib/standard_id.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c487f3a2265110f88e093a27b30e78742ecc04cbf460e94cbe35df02dbd2621
|
|
4
|
+
data.tar.gz: 6ad0ecc9edb3a9c2e5cbc51096ac08f6164b7bcb4671098f85806d3a1f605f6e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 818624622fef93866c46945cbe85c5a5e5898aea0d9770dab65596ab2524f03470f3f51b26286cea93a3cca639823be9566a7ef270cdbeec34794c6ebd4772e0
|
|
7
|
+
data.tar.gz: cea418b10ae16fa7d90caa16dfc4c394aa9445d4b6085380d38950c15c1d381a17b7bace3e786327fd21ac82b165525a95ad73b72a087b6cbb0b3c78ef13bf4a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.47.0] - 2026-10-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`c.oauth.disabled_grant_types`** (array, default `[]`): grant types the token endpoint refuses even though the engine implements them. A disabled grant answers exactly like an unknown one — 400 `unsupported_grant_type`, "Unsupported grant_type: <name>" — before the flow's parameters are read, so a caller cannot distinguish "disabled" from "never implemented", and both discovery documents drop it from `grant_types_supported`. Hosts that routed email+password sign-in through their own API and closed the resource-owner password grant with a `prepend` on `PasswordFlow` / `TokensController` (luminality-web `config/initializers/standard_id_password_grant.rb`) can replace the patch with `c.oauth.disabled_grant_types = %w[password]`.
|
|
15
|
+
- **`c.oauth.password_grant_requires_verified_identifier`** (boolean, default `false`): when on, `grant_type=password` refuses a credential whose identifier is unverified with the same 400 `invalid_grant` / "Invalid username or password" it gives a wrong password, and publishes `AUTHENTICATION_FAILED` with `error_code: "identifier_unverified"`. Until now the grant minted tokens for a sign-up that never confirmed its email, bypassing verification the host enforces on its other login paths. Off by default so an upgrade changes nothing; turn it on wherever the grant stays enabled.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- `PasswordFlow#emit_authentication_failed` takes optional `error_code:` / `error_message:` (defaults unchanged) so the verified-identifier refusal can be told apart in audit subscribers.
|
|
20
|
+
- `StandardId::Oauth::GrantTypes` (`.disabled`, `.disabled?(name)`) is the single reader of `oauth.disabled_grant_types`, used by both the token endpoint and the discovery documents so they cannot normalise the list differently (#375).
|
|
21
|
+
|
|
10
22
|
## [0.46.0] - 2026-10-05
|
|
11
23
|
|
|
12
24
|
### Added
|
data/README.md
CHANGED
|
@@ -189,6 +189,24 @@ StandardId.configure do |config|
|
|
|
189
189
|
end
|
|
190
190
|
```
|
|
191
191
|
|
|
192
|
+
Two further settings close the resource-owner password grant, or harden it, for
|
|
193
|
+
hosts whose email+password sign-in lives in their own API:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
StandardId.configure do |config|
|
|
197
|
+
# Refuse grant types you do not want on the token endpoint. A disabled grant
|
|
198
|
+
# answers exactly like an unknown one (400 unsupported_grant_type, before the
|
|
199
|
+
# flow's params are read) and is dropped from `grant_types_supported` in both
|
|
200
|
+
# discovery documents. Values are the grant_type strings.
|
|
201
|
+
config.oauth.disabled_grant_types = %w[password]
|
|
202
|
+
|
|
203
|
+
# If the password grant stays enabled: refuse credentials whose identifier is
|
|
204
|
+
# not verified, with the same invalid_grant answer as a wrong password, and
|
|
205
|
+
# publish AUTHENTICATION_FAILED with error_code "identifier_unverified".
|
|
206
|
+
config.oauth.password_grant_requires_verified_identifier = true
|
|
207
|
+
end
|
|
208
|
+
```
|
|
209
|
+
|
|
192
210
|
Under `:grant`, revoking a **stateless access token is a no-op** — this engine
|
|
193
211
|
has no per-access-token store, so it cannot invalidate one before its `exp`
|
|
194
212
|
(the response is still `200`, per RFC 7009 §2.2). Clients that need revocation
|
|
@@ -86,7 +86,9 @@ module StandardId
|
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
klass = FLOW_STRATEGIES[grant_type]
|
|
89
|
-
|
|
89
|
+
# A disabled grant is indistinguishable from an unknown one: same
|
|
90
|
+
# error, same message, raised before the flow's params are read.
|
|
91
|
+
if klass.nil? || StandardId::Oauth::GrantTypes.disabled?(grant_type)
|
|
90
92
|
raise StandardId::UnsupportedGrantTypeError, "Unsupported grant_type: #{grant_type}"
|
|
91
93
|
end
|
|
92
94
|
klass
|
|
@@ -457,6 +457,33 @@ StandardId::ConfigSchema.define do
|
|
|
457
457
|
# immediately.
|
|
458
458
|
field :introspection_enabled, type: :boolean, default: false
|
|
459
459
|
|
|
460
|
+
# Grant types the token endpoint refuses even though the engine implements
|
|
461
|
+
# them. Values are the `grant_type` strings: "authorization_code",
|
|
462
|
+
# "refresh_token", "client_credentials", "password", "passwordless_otp".
|
|
463
|
+
#
|
|
464
|
+
# A disabled grant answers exactly like an unknown one — 400
|
|
465
|
+
# `unsupported_grant_type`, "Unsupported grant_type: <name>" — before the
|
|
466
|
+
# flow's parameters are read, so a caller cannot tell a disabled grant from
|
|
467
|
+
# one that was never implemented, and the discovery documents drop it from
|
|
468
|
+
# `grant_types_supported`. Typical use: a host whose email+password sign-in
|
|
469
|
+
# runs through its own API (verification, per-email limits, generic
|
|
470
|
+
# answers) and must not leave the resource-owner password grant open as a
|
|
471
|
+
# second, weaker door to the same credentials.
|
|
472
|
+
#
|
|
473
|
+
# Empty (default) = every implemented grant is available, as before.
|
|
474
|
+
field :disabled_grant_types, type: :array, default: -> { [] }
|
|
475
|
+
|
|
476
|
+
# When true, the resource-owner password grant refuses a credential whose
|
|
477
|
+
# identifier has not been verified (`StandardId::Identifier#verified_at`
|
|
478
|
+
# nil) with the same 400 `invalid_grant` / "Invalid username or password"
|
|
479
|
+
# it gives a wrong password, and publishes AUTHENTICATION_FAILED with
|
|
480
|
+
# `error_code: "identifier_unverified"`. Without it a sign-up that never
|
|
481
|
+
# confirmed its email can mint tokens through `grant_type=password`,
|
|
482
|
+
# bypassing whatever verification the host enforces on its web and API
|
|
483
|
+
# login paths. Default false so an upgrade changes nothing; turn it on
|
|
484
|
+
# wherever the grant stays enabled.
|
|
485
|
+
field :password_grant_requires_verified_identifier, type: :boolean, default: false
|
|
486
|
+
|
|
460
487
|
# Where the discovery documents say the ENDPOINTS live.
|
|
461
488
|
#
|
|
462
489
|
# The issuer and the endpoint base are different things. The issuer is a
|
|
@@ -64,7 +64,9 @@ module StandardId
|
|
|
64
64
|
revocation_endpoint: "#{base}/oauth/revoke",
|
|
65
65
|
userinfo_endpoint: "#{base}/userinfo",
|
|
66
66
|
response_types_supported: %w[code],
|
|
67
|
-
|
|
67
|
+
# Minus anything the host turned off with `oauth.disabled_grant_types`,
|
|
68
|
+
# so a client is never pointed at a grant the token endpoint refuses.
|
|
69
|
+
grant_types_supported: %w[authorization_code refresh_token client_credentials] - StandardId::Oauth::GrantTypes.disabled,
|
|
68
70
|
subject_types_supported: %w[public],
|
|
69
71
|
id_token_signing_alg_values_supported: [StandardId.config.oauth.signing_algorithm.to_s.upcase],
|
|
70
72
|
# "none" advertises public-client support (PKCE-only token exchange,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Oauth
|
|
3
|
+
# The one reader for `config.oauth.disabled_grant_types`, so the token
|
|
4
|
+
# endpoint (which refuses a disabled grant) and the discovery documents
|
|
5
|
+
# (which stop advertising it) can never normalise the list differently.
|
|
6
|
+
module GrantTypes
|
|
7
|
+
# @return [Array<String>] grant_type strings the host has turned off
|
|
8
|
+
def self.disabled
|
|
9
|
+
Array(StandardId.config.oauth.disabled_grant_types).map(&:to_s)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @param name [String, Symbol, nil] a `grant_type` value
|
|
13
|
+
def self.disabled?(name)
|
|
14
|
+
disabled.include?(name.to_s)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -19,6 +19,13 @@ module StandardId
|
|
|
19
19
|
raise StandardId::InvalidGrantError, "Invalid username or password"
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
# Same answer as a wrong password on purpose: the grant must not reveal
|
|
23
|
+
# which addresses exist but are unverified.
|
|
24
|
+
if unverified_identifier_refused?
|
|
25
|
+
emit_authentication_failed(error_code: "identifier_unverified", error_message: "Identifier not verified")
|
|
26
|
+
raise StandardId::InvalidGrantError, "Invalid username or password"
|
|
27
|
+
end
|
|
28
|
+
|
|
22
29
|
emit_password_validated
|
|
23
30
|
validate_requested_scope!
|
|
24
31
|
end
|
|
@@ -33,16 +40,23 @@ module StandardId
|
|
|
33
40
|
)
|
|
34
41
|
end
|
|
35
42
|
|
|
36
|
-
def emit_authentication_failed
|
|
43
|
+
def emit_authentication_failed(error_code: "invalid_credentials", error_message: "Invalid username or password")
|
|
37
44
|
StandardId::Events.publish(
|
|
38
45
|
StandardId::Events::AUTHENTICATION_FAILED,
|
|
39
46
|
account_lookup: params[:username],
|
|
40
47
|
auth_method: "password",
|
|
41
|
-
error_code:
|
|
42
|
-
error_message:
|
|
48
|
+
error_code: error_code,
|
|
49
|
+
error_message: error_message
|
|
43
50
|
)
|
|
44
51
|
end
|
|
45
52
|
|
|
53
|
+
def unverified_identifier_refused?
|
|
54
|
+
return false unless StandardId.config.oauth.password_grant_requires_verified_identifier
|
|
55
|
+
|
|
56
|
+
identifier = @credential&.credential&.identifier
|
|
57
|
+
identifier.nil? || !identifier.verified?
|
|
58
|
+
end
|
|
59
|
+
|
|
46
60
|
def emit_password_validated
|
|
47
61
|
StandardId::Events.publish(
|
|
48
62
|
StandardId::Events::PASSWORD_VALIDATED,
|
data/lib/standard_id/version.rb
CHANGED
data/lib/standard_id.rb
CHANGED
|
@@ -55,6 +55,7 @@ require "standard_id/oauth/subflows/traditional_code_grant"
|
|
|
55
55
|
require "standard_id/oauth/subflows/social_login_grant"
|
|
56
56
|
require "standard_id/oauth/passwordless_otp_flow"
|
|
57
57
|
require "standard_id/oauth/discovery_resolver"
|
|
58
|
+
require "standard_id/oauth/grant_types"
|
|
58
59
|
require "standard_id/oauth/discovery_document"
|
|
59
60
|
require "standard_id/routing"
|
|
60
61
|
require "standard_id/oauth/consent_payload"
|
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.
|
|
4
|
+
version: 0.47.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jaryl Sim
|
|
@@ -287,6 +287,7 @@ files:
|
|
|
287
287
|
- lib/standard_id/oauth/consent_payload.rb
|
|
288
288
|
- lib/standard_id/oauth/discovery_document.rb
|
|
289
289
|
- lib/standard_id/oauth/discovery_resolver.rb
|
|
290
|
+
- lib/standard_id/oauth/grant_types.rb
|
|
290
291
|
- lib/standard_id/oauth/implicit_authorization_flow.rb
|
|
291
292
|
- lib/standard_id/oauth/oauth_session_persistence.rb
|
|
292
293
|
- lib/standard_id/oauth/password_flow.rb
|