standard_id 0.26.3 → 0.27.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/concerns/standard_id/lifecycle_hooks.rb +3 -1
- data/app/controllers/standard_id/web/account_controller.rb +1 -1
- data/app/controllers/standard_id/web/base_controller.rb +15 -0
- data/app/controllers/standard_id/web/login_controller.rb +1 -1
- data/app/controllers/standard_id/web/login_verify_controller.rb +2 -2
- data/app/controllers/standard_id/web/reset_password/confirm_controller.rb +2 -2
- data/app/controllers/standard_id/web/reset_password/start_controller.rb +1 -1
- data/app/controllers/standard_id/web/sessions_controller.rb +2 -2
- data/app/models/standard_id/client_application.rb +39 -0
- 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: d2917c7c97e54e0fd55a78e52c0f8eb202e3e1221267d5fe25af54da788c0e85
|
|
4
|
+
data.tar.gz: 7d2dad4ae7677431a6df91efef81ef0cf08794d7adc26be6b26fede63247f358
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5bc7d5bc447389b6cb44ae9962820fcb6a4bfb2109dc1e82fb334cfdba08d8dab1c1f8d7c41e63ee47ec8962b3a2ee38bd59d02ada3177b6ede3adce246398a2
|
|
7
|
+
data.tar.gz: 7d27375bfce3b17ff6ede7690f6e114f0e84d374c792e02d6c847f90a8a5a6d4c5492a57df596a94d8dbae073aa39c67627498c23c05ddb3639517d580ad4437
|
|
@@ -197,7 +197,9 @@ module StandardId
|
|
|
197
197
|
# When raised without arguments, StandardError#message returns the class name
|
|
198
198
|
message = "Sign-in was denied" if message.blank? || message == error.class.name
|
|
199
199
|
login_path = begin
|
|
200
|
-
|
|
200
|
+
# Engine `_path` helpers are mount-relative and redirect_to won't prepend
|
|
201
|
+
# the mount's SCRIPT_NAME (no-op at root), so a non-root mount would 404.
|
|
202
|
+
"#{request.script_name}#{StandardId::WebEngine.routes.url_helpers.login_path}"
|
|
201
203
|
rescue NameError, NoMethodError, ActionController::UrlGenerationError
|
|
202
204
|
StandardId.config.login_url || "/"
|
|
203
205
|
end
|
|
@@ -16,7 +16,7 @@ module StandardId
|
|
|
16
16
|
@account = current_account
|
|
17
17
|
|
|
18
18
|
if @account.update(account_params)
|
|
19
|
-
redirect_to account_path, notice: "Account updated successfully"
|
|
19
|
+
redirect_to engine_path(account_path), notice: "Account updated successfully"
|
|
20
20
|
else
|
|
21
21
|
flash.now[:alert] = @account.errors.full_messages.join(", ")
|
|
22
22
|
render :edit, status: :unprocessable_content
|
|
@@ -36,6 +36,21 @@ module StandardId
|
|
|
36
36
|
redirect_to_login
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# Prefix an engine-relative path with the current mount point's SCRIPT_NAME.
|
|
40
|
+
#
|
|
41
|
+
# Isolated-engine `_path` helpers return paths relative to the engine mount
|
|
42
|
+
# (e.g. "/login_verify"), and `redirect_to` / `redirect_with_inertia` —
|
|
43
|
+
# unlike view URL generation (form_with / link_to / url_for) — do NOT
|
|
44
|
+
# prepend the mount's SCRIPT_NAME. So a bare `redirect_to login_verify_path`
|
|
45
|
+
# 404s when the engine is mounted at a non-root path (e.g. "/auth" yields
|
|
46
|
+
# "/login_verify" instead of "/auth/login_verify"). SCRIPT_NAME is "" for a
|
|
47
|
+
# root mount, so this is a no-op there. Apply ONLY to engine-relative paths
|
|
48
|
+
# — host destinations (after_authentication_url, safe_post_signin_default)
|
|
49
|
+
# are already absolute and must not be prefixed.
|
|
50
|
+
def engine_path(path)
|
|
51
|
+
"#{request.script_name}#{path}"
|
|
52
|
+
end
|
|
53
|
+
|
|
39
54
|
# Read a top-level query/form param expected to be a scalar String, returning
|
|
40
55
|
# nil for absent/blank values OR if Rails parsed it as an Array/Hash (e.g. from
|
|
41
56
|
# `?redirect_uri[]=a&redirect_uri[]=b`). Without this guard, `redirect_to` is
|
|
@@ -108,7 +108,7 @@ module StandardId
|
|
|
108
108
|
redirect_uri = string_param(:redirect_uri)
|
|
109
109
|
session[:return_to_after_authenticating] = redirect_uri if redirect_uri
|
|
110
110
|
|
|
111
|
-
redirect_to login_verify_path, status: :see_other
|
|
111
|
+
redirect_to engine_path(login_verify_path), status: :see_other
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
def redirect_if_authenticated
|
|
@@ -94,7 +94,7 @@ module StandardId
|
|
|
94
94
|
signed_payload = session[:standard_id_otp_payload]
|
|
95
95
|
|
|
96
96
|
if signed_payload.blank?
|
|
97
|
-
redirect_to login_path, alert: "Please start the login process"
|
|
97
|
+
redirect_to engine_path(login_path), alert: "Please start the login process"
|
|
98
98
|
return
|
|
99
99
|
end
|
|
100
100
|
|
|
@@ -102,7 +102,7 @@ module StandardId
|
|
|
102
102
|
@otp_data = Rails.application.message_verifier(:otp).verify(signed_payload).symbolize_keys
|
|
103
103
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
|
104
104
|
session.delete(:standard_id_otp_payload)
|
|
105
|
-
redirect_to login_path, alert: "Your verification session has expired. Please try again."
|
|
105
|
+
redirect_to engine_path(login_path), alert: "Your verification session has expired. Please try again."
|
|
106
106
|
end
|
|
107
107
|
end
|
|
108
108
|
|
|
@@ -20,7 +20,7 @@ module StandardId
|
|
|
20
20
|
|
|
21
21
|
if form.submit
|
|
22
22
|
flash[:notice] = "Your password has been successfully reset. Please sign in with your new password."
|
|
23
|
-
redirect_to login_path, status: :see_other
|
|
23
|
+
redirect_to engine_path(login_path), status: :see_other
|
|
24
24
|
else
|
|
25
25
|
flash.now[:alert] = form.errors.full_messages.to_sentence
|
|
26
26
|
render :show, status: :unprocessable_content
|
|
@@ -41,7 +41,7 @@ module StandardId
|
|
|
41
41
|
return if @password_credential.present?
|
|
42
42
|
|
|
43
43
|
flash[:alert] = "Invalid or expired password reset link"
|
|
44
|
-
redirect_to login_path, status: :see_other
|
|
44
|
+
redirect_to engine_path(login_path), status: :see_other
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
end
|
|
@@ -21,7 +21,7 @@ module StandardId
|
|
|
21
21
|
|
|
22
22
|
if form.submit
|
|
23
23
|
flash[:notice] = "If an account with that email exists, we've sent password reset instructions."
|
|
24
|
-
redirect_to login_path, status: :see_other
|
|
24
|
+
redirect_to engine_path(login_path), status: :see_other
|
|
25
25
|
else
|
|
26
26
|
flash.now[:alert] = form.errors[:email].first || "Please enter your email address"
|
|
27
27
|
render :show, status: :unprocessable_content
|
|
@@ -19,10 +19,10 @@ module StandardId
|
|
|
19
19
|
else
|
|
20
20
|
# Revoke other session
|
|
21
21
|
session.revoke!
|
|
22
|
-
redirect_to sessions_path, notice: "Session revoked successfully"
|
|
22
|
+
redirect_to engine_path(sessions_path), notice: "Session revoked successfully"
|
|
23
23
|
end
|
|
24
24
|
rescue ActiveRecord::RecordNotFound
|
|
25
|
-
redirect_to sessions_path, alert: "Session not found"
|
|
25
|
+
redirect_to engine_path(sessions_path), alert: "Session not found"
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
end
|
|
@@ -34,6 +34,11 @@ module StandardId
|
|
|
34
34
|
scope :public_clients, -> { where(client_type: "public") }
|
|
35
35
|
scope :for_owner, ->(owner) { where(owner: owner) }
|
|
36
36
|
|
|
37
|
+
# Loopback interface hosts per RFC 8252 §7.3 (native apps). "localhost" is
|
|
38
|
+
# included for compatibility but RFC 8252 §8.3 recommends clients use
|
|
39
|
+
# 127.0.0.1/::1 instead, since "localhost" can be remapped by the OS.
|
|
40
|
+
LOOPBACK_HOSTS = %w[127.0.0.1 ::1 localhost].freeze
|
|
41
|
+
|
|
37
42
|
# Callbacks
|
|
38
43
|
before_create :generate_client_id
|
|
39
44
|
before_update :set_deactivated_at, if: :will_save_change_to_active?
|
|
@@ -99,6 +104,12 @@ module StandardId
|
|
|
99
104
|
# (or, worse, a different path segment like /cb/evil).
|
|
100
105
|
#
|
|
101
106
|
# Subdomain wildcards are NOT supported — host must match exactly.
|
|
107
|
+
#
|
|
108
|
+
# Exception — loopback redirects for native apps (RFC 8252 §7.3): when this
|
|
109
|
+
# client is public + PKCE-required and BOTH the registered and requested
|
|
110
|
+
# URIs are http loopback URIs, the port is ignored (native apps bind an
|
|
111
|
+
# ephemeral port on a local listener at authorization time, so it cannot be
|
|
112
|
+
# known at registration). See #loopback_redirect_uri? below.
|
|
102
113
|
def valid_redirect_uri?(uri)
|
|
103
114
|
requested = self.class.parse_redirect_uri(uri)
|
|
104
115
|
return false unless requested
|
|
@@ -107,6 +118,23 @@ module StandardId
|
|
|
107
118
|
registered = self.class.parse_redirect_uri(registered_uri)
|
|
108
119
|
next false unless registered
|
|
109
120
|
|
|
121
|
+
# RFC 8252 §7.3: for loopback interface redirects, "the authorization
|
|
122
|
+
# server MUST allow any port to be specified at the time of the request".
|
|
123
|
+
# Only host + path are compared; scheme is already pinned to "http" by
|
|
124
|
+
# the loopback predicate. Host equality is still required, so a client
|
|
125
|
+
# registered with 127.0.0.1 does not match localhost (or vice versa) —
|
|
126
|
+
# per §8.3, "localhost" is less trustworthy than the literal loopback
|
|
127
|
+
# IPs because the OS can remap it. This relaxation is gated to public
|
|
128
|
+
# PKCE clients: the redirect lands on an ephemeral listener on the
|
|
129
|
+
# user's own machine and PKCE binds the code to the initiating client,
|
|
130
|
+
# whereas confidential clients have stable callback URLs and keep
|
|
131
|
+
# strict port matching.
|
|
132
|
+
if public? && require_pkce? &&
|
|
133
|
+
self.class.loopback_redirect_uri?(registered) &&
|
|
134
|
+
self.class.loopback_redirect_uri?(requested)
|
|
135
|
+
next registered.host == requested.host && registered.path == requested.path
|
|
136
|
+
end
|
|
137
|
+
|
|
110
138
|
registered.scheme == requested.scheme &&
|
|
111
139
|
registered.host == requested.host &&
|
|
112
140
|
registered.port == requested.port &&
|
|
@@ -114,6 +142,17 @@ module StandardId
|
|
|
114
142
|
end
|
|
115
143
|
end
|
|
116
144
|
|
|
145
|
+
# True when the parsed URI is an http URI targeting a loopback interface
|
|
146
|
+
# literal (RFC 8252 §7.3). IPv6 loopback hosts are normalized: URI.parse
|
|
147
|
+
# yields "[::1]" on some Ruby versions and "::1" on others, so surrounding
|
|
148
|
+
# brackets are stripped before comparison.
|
|
149
|
+
def self.loopback_redirect_uri?(parsed_uri)
|
|
150
|
+
return false unless parsed_uri.scheme == "http"
|
|
151
|
+
|
|
152
|
+
host = parsed_uri.host.to_s.delete_prefix("[").delete_suffix("]")
|
|
153
|
+
LOOPBACK_HOSTS.include?(host)
|
|
154
|
+
end
|
|
155
|
+
|
|
117
156
|
# Parse a redirect URI string into a URI object suitable for comparison.
|
|
118
157
|
# Returns nil for unparseable, relative, or scheme-less URIs.
|
|
119
158
|
def self.parse_redirect_uri(value)
|
data/lib/standard_id/version.rb
CHANGED