standard_id 0.23.0 → 0.24.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/app/controllers/standard_id/web/consent_controller.rb +17 -2
- data/lib/standard_id/config/schema.rb +15 -0
- data/lib/standard_id/oauth/authorization_code_flow.rb +24 -6
- data/lib/standard_id/oauth/client_registration.rb +18 -3
- data/lib/standard_id/oauth/discovery_document.rb +3 -1
- data/lib/standard_id/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96bb41b62e41b340083b98a19c91913d8a1342a1895150d68595cba6a576d039
|
|
4
|
+
data.tar.gz: b85e690728fad61be9a1ab5fa047096cf3db2b97bea9b9403774cd53895216f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4bebc333835970bebc067517014364dce32cfa6f3d9165a64aa7b82f3464f5862439c9fd7e40d73ae6d79a478490abe749609804c168d9ae8669d29e7d6d861e
|
|
7
|
+
data.tar.gz: 23a59d0a655a3d933ed953923ce5f2c4f63240543b06f094faa182366c6d147e27b769c6c6230821d0a1c60501399ac7569296840255403b95953faf37a5ba8b
|
|
@@ -84,11 +84,26 @@ module StandardId
|
|
|
84
84
|
.new(@consent_request, request, current_account: current_account)
|
|
85
85
|
.execute
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
redirect_out(result[:redirect_to], status: result[:status] || :found)
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
def deny!
|
|
91
|
-
|
|
91
|
+
redirect_out(denied_redirect_uri, status: :found)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Redirect to the OAuth client's (external) redirect_uri in a way that
|
|
95
|
+
# works for BOTH plain-browser and Inertia consumers of the consent
|
|
96
|
+
# screen. An Inertia visit is an XHR — it cannot follow a 302 to a
|
|
97
|
+
# non-Inertia, cross-origin URL (the browser stays on the consent page),
|
|
98
|
+
# so for Inertia requests we emit an Inertia-Location (409 +
|
|
99
|
+
# X-Inertia-Location) which the client turns into a hard `window.location`
|
|
100
|
+
# navigation. Plain (ERB) form posts keep the ordinary redirect.
|
|
101
|
+
def redirect_out(url, status: :found)
|
|
102
|
+
if request.respond_to?(:inertia?) && request.inertia?
|
|
103
|
+
inertia_location(url)
|
|
104
|
+
else
|
|
105
|
+
redirect_to url, status: status, allow_other_host: true
|
|
106
|
+
end
|
|
92
107
|
end
|
|
93
108
|
|
|
94
109
|
def denied_redirect_uri
|
|
@@ -289,6 +289,21 @@ StandardId::ConfigSchema.define do
|
|
|
289
289
|
# silently failing the model's presence validation — so misconfiguration is
|
|
290
290
|
# caught loudly at request time.
|
|
291
291
|
field :dynamic_registration_owner, type: :any, default: nil
|
|
292
|
+
|
|
293
|
+
# Default `token_endpoint_auth_method` applied to clients created via RFC 7591
|
|
294
|
+
# Dynamic Client Registration when the request omits `token_endpoint_auth_method`.
|
|
295
|
+
#
|
|
296
|
+
# Controls whether self-registered clients default to PUBLIC (PKCE-only, no
|
|
297
|
+
# secret) or to a CONFIDENTIAL secret-bearing method. The default `"none"`
|
|
298
|
+
# preserves the historical behaviour (DCR clients are public unless they ask
|
|
299
|
+
# for a secret) and is the right default for interactive/native/MCP clients,
|
|
300
|
+
# which cannot keep a secret.
|
|
301
|
+
#
|
|
302
|
+
# Valid values (validated at use in StandardId::Oauth::ClientRegistration):
|
|
303
|
+
# "none" — public client, authenticates via PKCE alone
|
|
304
|
+
# "client_secret_basic" — confidential, secret via HTTP Basic
|
|
305
|
+
# "client_secret_post" — confidential, secret in the request body
|
|
306
|
+
field :dynamic_registration_default_auth_method, type: :string, default: "none"
|
|
292
307
|
end
|
|
293
308
|
|
|
294
309
|
scope :social do
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
module StandardId
|
|
2
2
|
module Oauth
|
|
3
3
|
class AuthorizationCodeFlow < TokenGrantFlow
|
|
4
|
-
expect_params :client_id, :
|
|
5
|
-
permit_params :redirect_uri, :code_verifier
|
|
4
|
+
expect_params :client_id, :code
|
|
5
|
+
permit_params :client_secret, :redirect_uri, :code_verifier
|
|
6
6
|
|
|
7
7
|
def authenticate!
|
|
8
|
-
@
|
|
8
|
+
@client = StandardId::ClientApplication.find_by(client_id: params[:client_id])
|
|
9
|
+
raise StandardId::InvalidClientError, "Client authentication failed" if @client.nil?
|
|
10
|
+
|
|
11
|
+
# Confidential clients authenticate with a client secret. Public clients
|
|
12
|
+
# (e.g. native/SPA/MCP clients per RFC 8252 / OAuth 2.1) cannot keep a
|
|
13
|
+
# secret and authenticate via PKCE alone — they MUST NOT send one.
|
|
14
|
+
if @client.confidential?
|
|
15
|
+
@credential = validate_client_secret!(params[:client_id], params[:client_secret])
|
|
16
|
+
elsif params[:client_secret].present?
|
|
17
|
+
raise StandardId::InvalidClientError, "Public clients must not send a client_secret"
|
|
18
|
+
end
|
|
9
19
|
|
|
10
20
|
@authorization_code = find_authorization_code(params[:code])
|
|
11
21
|
unless @authorization_code&.valid_for_client?(params[:client_id])
|
|
@@ -16,6 +26,14 @@ module StandardId
|
|
|
16
26
|
raise StandardId::InvalidGrantError, "Redirect URI mismatch"
|
|
17
27
|
end
|
|
18
28
|
|
|
29
|
+
# Fail closed: a public client's only authentication factor is PKCE, so a
|
|
30
|
+
# code minted without a code_challenge offers no client authentication at
|
|
31
|
+
# all. (pkce_valid? returns true when code_challenge is blank, which is
|
|
32
|
+
# safe for confidential clients but would be a bypass for public ones.)
|
|
33
|
+
if @client.public? && @authorization_code.code_challenge.blank?
|
|
34
|
+
raise StandardId::InvalidGrantError, "PKCE is required for public clients"
|
|
35
|
+
end
|
|
36
|
+
|
|
19
37
|
unless @authorization_code.pkce_valid?(params[:code_verifier])
|
|
20
38
|
raise StandardId::InvalidGrantError, "Invalid PKCE code_verifier"
|
|
21
39
|
end
|
|
@@ -30,7 +48,7 @@ module StandardId
|
|
|
30
48
|
StandardId::Events.publish(
|
|
31
49
|
StandardId::Events::OAUTH_CODE_CONSUMED,
|
|
32
50
|
authorization_code: @authorization_code,
|
|
33
|
-
client_id: @
|
|
51
|
+
client_id: @client.client_id,
|
|
34
52
|
account: @authorization_code.account
|
|
35
53
|
)
|
|
36
54
|
end
|
|
@@ -40,7 +58,7 @@ module StandardId
|
|
|
40
58
|
end
|
|
41
59
|
|
|
42
60
|
def client_id
|
|
43
|
-
@
|
|
61
|
+
@client.client_id
|
|
44
62
|
end
|
|
45
63
|
|
|
46
64
|
def token_scope
|
|
@@ -60,7 +78,7 @@ module StandardId
|
|
|
60
78
|
end
|
|
61
79
|
|
|
62
80
|
def token_client
|
|
63
|
-
@
|
|
81
|
+
@client
|
|
64
82
|
end
|
|
65
83
|
|
|
66
84
|
def token_account
|
|
@@ -25,7 +25,7 @@ module StandardId
|
|
|
25
25
|
# token_endpoint_auth_method -> client_type mapping.
|
|
26
26
|
PUBLIC_AUTH_METHOD = "none".freeze
|
|
27
27
|
CONFIDENTIAL_AUTH_METHODS = %w[client_secret_basic client_secret_post].freeze
|
|
28
|
-
|
|
28
|
+
SUPPORTED_AUTH_METHODS = (CONFIDENTIAL_AUTH_METHODS + [PUBLIC_AUTH_METHOD]).freeze
|
|
29
29
|
DEFAULT_SCOPE = "openid profile email".freeze
|
|
30
30
|
|
|
31
31
|
# Minimal result object mirroring the gem's `result.success?` /
|
|
@@ -132,7 +132,22 @@ module StandardId
|
|
|
132
132
|
|
|
133
133
|
def auth_method
|
|
134
134
|
method = metadata[:token_endpoint_auth_method].to_s.strip
|
|
135
|
-
method.presence ||
|
|
135
|
+
method.presence || default_auth_method
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Fallback token_endpoint_auth_method when the registration request omits
|
|
139
|
+
# one. Reads the first-class config value (default "none"). A misconfigured
|
|
140
|
+
# value is a host-app bug, so it raises ConfigurationError loudly rather
|
|
141
|
+
# than silently registering a malformed client.
|
|
142
|
+
def default_auth_method
|
|
143
|
+
configured = StandardId.config.oauth.dynamic_registration_default_auth_method.to_s
|
|
144
|
+
unless SUPPORTED_AUTH_METHODS.include?(configured)
|
|
145
|
+
raise StandardId::ConfigurationError,
|
|
146
|
+
"oauth.dynamic_registration_default_auth_method must be one of " \
|
|
147
|
+
"#{SUPPORTED_AUTH_METHODS.join(', ')} (got #{configured.inspect})"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
configured
|
|
136
151
|
end
|
|
137
152
|
|
|
138
153
|
def client_type
|
|
@@ -142,7 +157,7 @@ module StandardId
|
|
|
142
157
|
|
|
143
158
|
raise StandardId::InvalidClientMetadataError,
|
|
144
159
|
"Unsupported token_endpoint_auth_method: #{method.inspect}. " \
|
|
145
|
-
"Allowed: #{
|
|
160
|
+
"Allowed: #{SUPPORTED_AUTH_METHODS.join(', ')}"
|
|
146
161
|
end
|
|
147
162
|
|
|
148
163
|
# Public clients are always forced onto PKCE/S256 (the model also validates
|
|
@@ -39,7 +39,9 @@ module StandardId
|
|
|
39
39
|
grant_types_supported: %w[authorization_code refresh_token client_credentials],
|
|
40
40
|
subject_types_supported: %w[public],
|
|
41
41
|
id_token_signing_alg_values_supported: [StandardId.config.oauth.signing_algorithm.to_s.upcase],
|
|
42
|
-
|
|
42
|
+
# "none" advertises public-client support (PKCE-only token exchange,
|
|
43
|
+
# no client_secret) per RFC 8414 — required by native/SPA/MCP clients.
|
|
44
|
+
token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post none],
|
|
43
45
|
# PKCE is always enforced (require_pkce defaults true and cannot be
|
|
44
46
|
# disabled for public clients), so advertise the supported method.
|
|
45
47
|
code_challenge_methods_supported: %w[S256]
|
data/lib/standard_id/version.rb
CHANGED