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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed15fb7068e789bc80676ae7d191b1b154c79adeedc60a58aa55c3c5e5e1c29f
4
- data.tar.gz: 95cce0258343786169a8afcda56c91534e96f684b74f2764b7c8a66d838c4aab
3
+ metadata.gz: 96bb41b62e41b340083b98a19c91913d8a1342a1895150d68595cba6a576d039
4
+ data.tar.gz: b85e690728fad61be9a1ab5fa047096cf3db2b97bea9b9403774cd53895216f6
5
5
  SHA512:
6
- metadata.gz: 41efa2cbf81c06d2bbc8747b68bb8f1f76a13684eda0512710d4bdc756132cfd1765c7b2d1580d938e326eb3ad19e04e6b627cc86d27f27bb1c718803864cadb
7
- data.tar.gz: 07c359414b5800fd8151e5915bef1c51044cca4285c796c16c43e05c9593f903c8071c6da257d826cb3273d678c35faed27700807442f67e16ee1cc2cf3bcd28
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
- redirect_to result[:redirect_to], status: result[:status] || :found, allow_other_host: true
87
+ redirect_out(result[:redirect_to], status: result[:status] || :found)
88
88
  end
89
89
 
90
90
  def deny!
91
- redirect_to denied_redirect_uri, status: :found, allow_other_host: true
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, :client_secret, :code
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
- @credential = validate_client_secret!(params[:client_id], params[:client_secret])
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: @credential.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
- @credential.client_id
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
- @credential&.client_application
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
- DEFAULT_AUTH_METHOD = PUBLIC_AUTH_METHOD
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 || DEFAULT_AUTH_METHOD
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: #{(CONFIDENTIAL_AUTH_METHODS + [PUBLIC_AUTH_METHOD]).join(', ')}"
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
- token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post],
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]
@@ -1,3 +1,3 @@
1
1
  module StandardId
2
- VERSION = "0.23.0"
2
+ VERSION = "0.24.0"
3
3
  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.23.0
4
+ version: 0.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim