standard_id 0.26.4 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80484283cdfb54ecc2ead53d1498b5cd9ee88813e755b972db2e2d15561362f7
4
- data.tar.gz: 9f9aaea4b18b29a61c3d3e70d7be8f1c2016455b3866181b9bea0cd272617405
3
+ metadata.gz: d2917c7c97e54e0fd55a78e52c0f8eb202e3e1221267d5fe25af54da788c0e85
4
+ data.tar.gz: 7d2dad4ae7677431a6df91efef81ef0cf08794d7adc26be6b26fede63247f358
5
5
  SHA512:
6
- metadata.gz: 203398f53eacaf60b272dbaf84794c71d2a975fbd1f49669d37dc3375de6fa7d24f00b36f352a9bb09909e82e1947f4513082b397b61a664e22f0180aa34f665
7
- data.tar.gz: 737d854a5784cf508954f343482e0476e2f5ad7a40322a26d7e0360c6910af6333a954bab7e9f490fb66ffc77ee15946dff62e2afd4a5d67387c9aca60e04cf4
6
+ metadata.gz: 5bc7d5bc447389b6cb44ae9962820fcb6a4bfb2109dc1e82fb334cfdba08d8dab1c1f8d7c41e63ee47ec8962b3a2ee38bd59d02ada3177b6ede3adce246398a2
7
+ data.tar.gz: 7d27375bfce3b17ff6ede7690f6e114f0e84d374c792e02d6c847f90a8a5a6d4c5492a57df596a94d8dbae073aa39c67627498c23c05ddb3639517d580ad4437
@@ -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)
@@ -1,3 +1,3 @@
1
1
  module StandardId
2
- VERSION = "0.26.4"
2
+ VERSION = "0.27.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.26.4
4
+ version: 0.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim