authproto 0.3.0.beta1 → 0.3.0.beta2
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/.yardopts +1 -0
- data/AGENTS.md +80 -0
- data/CHANGELOG.md +48 -7
- data/README.md +20 -0
- data/examples/confidential_client/.gitignore +2 -1
- data/examples/confidential_client/Gemfile.lock +5 -1
- data/examples/confidential_client/README.md +2 -2
- data/examples/confidential_client/app.rb +17 -17
- data/examples/confidential_client/config/{client-metadata.example.json → oauth-client-metadata.example.json} +0 -1
- data/examples/confidential_client/scripts/generate_keys.rb +15 -9
- data/examples/minisky_console/Gemfile.lock +2 -2
- data/examples/minisky_console/localhost.rb +8 -25
- data/examples/minisky_console/production.rb +1 -1
- data/lib/authproto/client.rb +150 -201
- data/lib/authproto/dpop/client.rb +12 -60
- data/lib/authproto/dpop/key_manager.rb +17 -17
- data/lib/authproto/dpop/nonce_manager.rb +5 -19
- data/lib/authproto/dpop/proof_generator.rb +27 -23
- data/lib/authproto/errors.rb +1 -1
- data/lib/authproto/http_client.rb +17 -31
- data/lib/authproto/localhost_server.rb +15 -6
- data/lib/authproto/metadata/authorization_server.rb +358 -0
- data/lib/authproto/metadata/client_metadata.rb +574 -0
- data/lib/authproto/metadata/resource_server.rb +137 -0
- data/lib/authproto/{server_metadata.rb → metadata.rb} +7 -7
- data/lib/authproto/minisky.rb +8 -10
- data/lib/authproto/par/client.rb +51 -55
- data/lib/authproto/par/client_assertion.rb +58 -15
- data/lib/authproto/par/request.rb +58 -140
- data/lib/authproto/par/response.rb +9 -10
- data/lib/authproto/par.rb +3 -5
- data/lib/authproto/pkce.rb +29 -106
- data/lib/authproto/serialization/session.rb +11 -11
- data/lib/authproto/serialization/token_set.rb +6 -6
- data/lib/authproto/state/session.rb +27 -31
- data/lib/authproto/state/session_manager.rb +2 -2
- data/lib/authproto/state/token_set.rb +4 -4
- data/lib/authproto/state.rb +3 -3
- data/lib/authproto/token/refresh.rb +35 -43
- data/lib/authproto/validated_uri.rb +57 -0
- data/lib/authproto/version.rb +1 -1
- data/lib/authproto.rb +1 -0
- data/misc/yard_attributes.rb +34 -0
- data/sig/authproto/client.rbs +9 -10
- data/sig/authproto/dpop/client.rbs +1 -8
- data/sig/authproto/dpop/nonce_manager.rbs +0 -1
- data/sig/authproto/dpop/proof_generator.rbs +5 -5
- data/sig/authproto/http_client.rbs +2 -2
- data/sig/authproto/{server_metadata → metadata}/authorization_server.rbs +18 -12
- data/sig/authproto/metadata/client_metadata.rbs +118 -0
- data/sig/authproto/metadata/resource_server.rbs +27 -0
- data/sig/authproto/minisky_oauth.rbs +5 -3
- data/sig/authproto/par/client.rbs +4 -6
- data/sig/authproto/par/client_assertion.rbs +2 -1
- data/sig/authproto/par/request.rbs +5 -19
- data/sig/authproto/par/response.rbs +1 -5
- data/sig/authproto/pkce.rbs +6 -16
- data/sig/authproto/serialization/session.rbs +2 -2
- data/sig/authproto/state/session.rbs +4 -7
- data/sig/authproto/state/session_manager.rbs +2 -2
- data/sig/authproto/token/refresh.rbs +4 -4
- data/sig/authproto/validated_uri.rbs +36 -0
- data/sig/authproto.rbs +5 -3
- metadata +13 -11
- data/lib/authproto/client_metadata.rb +0 -478
- data/lib/authproto/server_metadata/authorization_server.rb +0 -267
- data/lib/authproto/server_metadata/origin_url.rb +0 -71
- data/lib/authproto/server_metadata/resource_server.rb +0 -98
- data/sig/authproto/client_metadata.rbs +0 -113
- data/sig/authproto/server_metadata/origin_url.rbs +0 -21
- data/sig/authproto/server_metadata/resource_server.rbs +0 -27
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'json'
|
|
4
|
-
require 'uri'
|
|
5
|
-
|
|
6
|
-
require_relative '../configuration'
|
|
7
|
-
require_relative '../errors'
|
|
8
|
-
require_relative '../http_client'
|
|
9
|
-
require_relative '../server_metadata'
|
|
10
|
-
require_relative 'origin_url'
|
|
11
|
-
|
|
12
|
-
module AuthProto
|
|
13
|
-
module ServerMetadata
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
# Handles fetching and validation of AT Protocol OAuth Authorization Server metadata.
|
|
17
|
-
# An Authorization Server in atproto can be either a PDS instance or a separate "entryway" server
|
|
18
|
-
# that handles authentication for multiple PDS instances.
|
|
19
|
-
#
|
|
20
|
-
# The Authorization Server metadata is fetched from the well-known endpoint
|
|
21
|
-
# /.well-known/oauth-authorization-server and must conform to RFC 8414 plus additional
|
|
22
|
-
# requirements specific to the AT Protocol OAuth profile.
|
|
23
|
-
#
|
|
24
|
-
# @example Fetching and validating Authorization Server metadata
|
|
25
|
-
# begin
|
|
26
|
-
# auth_server = AuthProto::ServerMetadata::AuthorizationServer.from_issuer("https://auth.example.com")
|
|
27
|
-
# puts "Authorization endpoint: #{auth_server.authorization_endpoint}"
|
|
28
|
-
# puts "Supported scopes: #{auth_server.scopes_supported}"
|
|
29
|
-
# rescue AuthProto::InvalidAuthorizationServer => e
|
|
30
|
-
# puts "Failed to validate authorization server: #{e.message}"
|
|
31
|
-
# end
|
|
32
|
-
#
|
|
33
|
-
# @see https://atproto.com/specs/oauth#authorization-servers Documentation of Authorization Server requirements
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
class AuthorizationServer
|
|
37
|
-
|
|
38
|
-
# Metadata keys that must be present before capability validation.
|
|
39
|
-
# @return [Array<String>]
|
|
40
|
-
REQUIRED_FIELDS = %w[
|
|
41
|
-
issuer
|
|
42
|
-
authorization_endpoint
|
|
43
|
-
token_endpoint
|
|
44
|
-
response_types_supported
|
|
45
|
-
grant_types_supported
|
|
46
|
-
code_challenge_methods_supported
|
|
47
|
-
token_endpoint_auth_methods_supported
|
|
48
|
-
token_endpoint_auth_signing_alg_values_supported
|
|
49
|
-
scopes_supported
|
|
50
|
-
dpop_signing_alg_values_supported
|
|
51
|
-
pushed_authorization_request_endpoint
|
|
52
|
-
].freeze
|
|
53
|
-
|
|
54
|
-
# Authorization server issuer origin.
|
|
55
|
-
# @return [String]
|
|
56
|
-
attr_reader :issuer
|
|
57
|
-
|
|
58
|
-
# HTTPS browser authorization endpoint.
|
|
59
|
-
# @return [String]
|
|
60
|
-
attr_reader :authorization_endpoint
|
|
61
|
-
|
|
62
|
-
# HTTPS token exchange and refresh endpoint.
|
|
63
|
-
# @return [String]
|
|
64
|
-
attr_reader :token_endpoint
|
|
65
|
-
|
|
66
|
-
# HTTPS PAR endpoint.
|
|
67
|
-
# @return [String]
|
|
68
|
-
attr_reader :pushed_authorization_request_endpoint
|
|
69
|
-
|
|
70
|
-
# Supported response types, including `code`.
|
|
71
|
-
# @return [Array<String>]
|
|
72
|
-
attr_reader :response_types_supported
|
|
73
|
-
|
|
74
|
-
# Supported grant types, including authorization code and refresh token.
|
|
75
|
-
# @return [Array<String>]
|
|
76
|
-
attr_reader :grant_types_supported
|
|
77
|
-
|
|
78
|
-
# Supported PKCE methods, including `S256`.
|
|
79
|
-
# @return [Array<String>]
|
|
80
|
-
attr_reader :code_challenge_methods_supported
|
|
81
|
-
|
|
82
|
-
# Supported client authentication methods.
|
|
83
|
-
# @return [Array<String>]
|
|
84
|
-
attr_reader :token_endpoint_auth_methods_supported
|
|
85
|
-
|
|
86
|
-
# Supported client assertion algorithms.
|
|
87
|
-
# @return [Array<String>]
|
|
88
|
-
attr_reader :token_endpoint_auth_signing_alg_values_supported
|
|
89
|
-
|
|
90
|
-
# Supported scopes, including `atproto`.
|
|
91
|
-
# @return [Array<String>]
|
|
92
|
-
attr_reader :scopes_supported
|
|
93
|
-
|
|
94
|
-
# Supported DPoP algorithms, including `ES256`.
|
|
95
|
-
# @return [Array<String>]
|
|
96
|
-
attr_reader :dpop_signing_alg_values_supported
|
|
97
|
-
|
|
98
|
-
# Validates an authorization server's OAuth metadata.
|
|
99
|
-
#
|
|
100
|
-
# @param metadata [Hash{String => Object}] parsed discovery document with the fields in {REQUIRED_FIELDS} and required Boolean capabilities
|
|
101
|
-
# @raise [InvalidAuthorizationServer] if required capabilities, issuer or endpoint schemes are invalid
|
|
102
|
-
# @raise [URI::InvalidURIError] if a metadata URL cannot be parsed
|
|
103
|
-
#
|
|
104
|
-
def initialize(metadata)
|
|
105
|
-
validate_and_set_metadata(metadata)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Fetches `/.well-known/oauth-authorization-server` using the configured transport.
|
|
109
|
-
#
|
|
110
|
-
# The document's issuer must exactly match the requested issuer.
|
|
111
|
-
#
|
|
112
|
-
# @param issuer [String] authorization server issuer origin
|
|
113
|
-
# @return [AuthorizationServer] validated discovery metadata
|
|
114
|
-
# @raise [InvalidAuthorizationServer] if HTTP transport fails, JSON is invalid, or metadata/issuer validation fails
|
|
115
|
-
# @raise [HTTPClient::SSRFError] if the transport rejects the initial destination
|
|
116
|
-
# @raise [URI::InvalidURIError] if the issuer or a metadata URI cannot be parsed
|
|
117
|
-
#
|
|
118
|
-
def self.from_issuer(issuer)
|
|
119
|
-
response = fetch_metadata(issuer)
|
|
120
|
-
metadata = parse_metadata(response[:body])
|
|
121
|
-
validate_issuer(metadata["issuer"], issuer)
|
|
122
|
-
new(metadata)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Exports metadata for serialization using symbol keys.
|
|
126
|
-
#
|
|
127
|
-
# Includes issuer/endpoints (String), capability lists (Array<String>), and the
|
|
128
|
-
# three required Boolean flags set to `true`. Use JSON serialization to obtain
|
|
129
|
-
# string keys suitable for passing back to {#initialize}.
|
|
130
|
-
#
|
|
131
|
-
# @return [Hash{Symbol => String, Array<String>, Boolean}] authorization server metadata
|
|
132
|
-
#
|
|
133
|
-
def to_h
|
|
134
|
-
{
|
|
135
|
-
issuer: issuer,
|
|
136
|
-
authorization_endpoint: authorization_endpoint,
|
|
137
|
-
token_endpoint: token_endpoint,
|
|
138
|
-
pushed_authorization_request_endpoint: pushed_authorization_request_endpoint,
|
|
139
|
-
response_types_supported: response_types_supported,
|
|
140
|
-
grant_types_supported: grant_types_supported,
|
|
141
|
-
code_challenge_methods_supported: code_challenge_methods_supported,
|
|
142
|
-
token_endpoint_auth_methods_supported: token_endpoint_auth_methods_supported,
|
|
143
|
-
token_endpoint_auth_signing_alg_values_supported: token_endpoint_auth_signing_alg_values_supported,
|
|
144
|
-
scopes_supported: scopes_supported,
|
|
145
|
-
dpop_signing_alg_values_supported: dpop_signing_alg_values_supported,
|
|
146
|
-
authorization_response_iss_parameter_supported: true,
|
|
147
|
-
require_pushed_authorization_requests: true,
|
|
148
|
-
client_id_metadata_document_supported: true
|
|
149
|
-
}
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
private
|
|
153
|
-
|
|
154
|
-
def validate_and_set_metadata(metadata)
|
|
155
|
-
REQUIRED_FIELDS.each do |field|
|
|
156
|
-
raise InvalidAuthorizationServer, "#{field} is required" unless metadata[field]
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
@issuer = validate_issuer(metadata["issuer"])
|
|
160
|
-
@authorization_endpoint = validate_https_url(metadata["authorization_endpoint"])
|
|
161
|
-
@token_endpoint = validate_https_url(metadata["token_endpoint"])
|
|
162
|
-
@pushed_authorization_request_endpoint = validate_https_url(metadata["pushed_authorization_request_endpoint"])
|
|
163
|
-
|
|
164
|
-
validate_response_types(metadata["response_types_supported"])
|
|
165
|
-
validate_grant_types(metadata["grant_types_supported"])
|
|
166
|
-
validate_code_challenge_methods(metadata["code_challenge_methods_supported"])
|
|
167
|
-
validate_token_endpoint_auth_methods(metadata["token_endpoint_auth_methods_supported"])
|
|
168
|
-
validate_token_endpoint_auth_signing_algs(metadata["token_endpoint_auth_signing_alg_values_supported"])
|
|
169
|
-
validate_dpop_signing_algs(metadata["dpop_signing_alg_values_supported"])
|
|
170
|
-
validate_scopes(metadata["scopes_supported"])
|
|
171
|
-
|
|
172
|
-
# Store validated values
|
|
173
|
-
@response_types_supported = metadata["response_types_supported"]
|
|
174
|
-
@grant_types_supported = metadata["grant_types_supported"]
|
|
175
|
-
@code_challenge_methods_supported = metadata["code_challenge_methods_supported"]
|
|
176
|
-
@token_endpoint_auth_methods_supported = metadata["token_endpoint_auth_methods_supported"]
|
|
177
|
-
@token_endpoint_auth_signing_alg_values_supported = metadata["token_endpoint_auth_signing_alg_values_supported"]
|
|
178
|
-
@scopes_supported = metadata["scopes_supported"]
|
|
179
|
-
@dpop_signing_alg_values_supported = metadata["dpop_signing_alg_values_supported"]
|
|
180
|
-
|
|
181
|
-
# Required boolean fields
|
|
182
|
-
validate_boolean_field(metadata, "authorization_response_iss_parameter_supported", true)
|
|
183
|
-
validate_boolean_field(metadata, "require_pushed_authorization_requests", true)
|
|
184
|
-
validate_boolean_field(metadata, "client_id_metadata_document_supported", true)
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def validate_issuer(issuer)
|
|
188
|
-
is_valid = OriginURL.new(issuer).valid?
|
|
189
|
-
raise InvalidAuthorizationServer, "invalid issuer URL format" unless is_valid
|
|
190
|
-
|
|
191
|
-
issuer
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def validate_https_url(url)
|
|
195
|
-
uri = URI(url)
|
|
196
|
-
raise InvalidAuthorizationServer, "URL must use HTTPS" unless uri.scheme == "https"
|
|
197
|
-
|
|
198
|
-
url
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def validate_response_types(types)
|
|
202
|
-
raise InvalidAuthorizationServer, "must support 'code' response type" unless types.include?("code")
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def validate_grant_types(types)
|
|
206
|
-
required = %w[authorization_code refresh_token]
|
|
207
|
-
missing = required - types
|
|
208
|
-
raise InvalidAuthorizationServer, "missing grant types: #{missing.join(", ")}" if missing.any?
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def validate_code_challenge_methods(methods)
|
|
212
|
-
raise InvalidAuthorizationServer, "must support S256 PKCE" unless methods.include?("S256")
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
def validate_token_endpoint_auth_methods(methods)
|
|
216
|
-
required = %w[private_key_jwt none]
|
|
217
|
-
missing = required - methods
|
|
218
|
-
raise InvalidAuthorizationServer, "missing auth methods: #{missing.join(", ")}" if missing.any?
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def validate_token_endpoint_auth_signing_algs(algs)
|
|
222
|
-
raise InvalidAuthorizationServer, "must support ES256" unless algs.include?("ES256")
|
|
223
|
-
raise InvalidAuthorizationServer, "must not allow 'none'" if algs.include?("none")
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
def validate_dpop_signing_algs(algs)
|
|
227
|
-
raise InvalidAuthorizationServer, "must support ES256 for DPoP" unless algs.include?("ES256")
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def validate_scopes(scopes)
|
|
231
|
-
required = %w[atproto]
|
|
232
|
-
missing = required - scopes
|
|
233
|
-
raise InvalidAuthorizationServer, "missing scopes: #{missing.join(", ")}" if missing.any?
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
def validate_boolean_field(metadata, field, required_value)
|
|
237
|
-
actual = metadata[field]
|
|
238
|
-
return if actual == required_value
|
|
239
|
-
|
|
240
|
-
raise InvalidAuthorizationServer, "#{field} must be #{required_value}"
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
class << self
|
|
244
|
-
private
|
|
245
|
-
|
|
246
|
-
def fetch_metadata(issuer)
|
|
247
|
-
metadata_url = URI.join(issuer, "/.well-known/oauth-authorization-server")
|
|
248
|
-
AuthProto.configuration.http_client.get(metadata_url.to_s)
|
|
249
|
-
rescue HTTPClient::HTTPError => e
|
|
250
|
-
raise InvalidAuthorizationServer, "Failed to fetch authorization server metadata: #{e.message}"
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
def parse_metadata(body)
|
|
254
|
-
JSON.parse(body)
|
|
255
|
-
rescue JSON::ParserError => e
|
|
256
|
-
raise InvalidAuthorizationServer, "Invalid JSON in authorization server metadata: #{e.message}"
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
def validate_issuer(metadata_issuer, request_issuer)
|
|
260
|
-
return if metadata_issuer == request_issuer
|
|
261
|
-
|
|
262
|
-
raise InvalidAuthorizationServer, "issuer mismatch: #{metadata_issuer} != #{request_issuer}"
|
|
263
|
-
end
|
|
264
|
-
end
|
|
265
|
-
end
|
|
266
|
-
end
|
|
267
|
-
end
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'uri'
|
|
4
|
-
|
|
5
|
-
require_relative '../server_metadata'
|
|
6
|
-
|
|
7
|
-
module AuthProto
|
|
8
|
-
module ServerMetadata
|
|
9
|
-
|
|
10
|
-
#
|
|
11
|
-
# The `OriginURL` class provides validation logic for URLs that must conform
|
|
12
|
-
# to the AT Protocol OAuth "simple origin URL" requirements. These requirements
|
|
13
|
-
# are common between Resource and Authorization Servers and ensure that the URL
|
|
14
|
-
# is valid and secure for use in the protocol. This class validates that the URL:
|
|
15
|
-
# - Uses the HTTPS scheme.
|
|
16
|
-
# - Points to the root path (either an empty path or "/").
|
|
17
|
-
# - Does not include a query string or fragment.
|
|
18
|
-
# - Does not include user or password credentials.
|
|
19
|
-
# - May include a non-default port but disallows the default HTTPS port (443).
|
|
20
|
-
#
|
|
21
|
-
# This model centralizes the URL validation logic to promote reusability and
|
|
22
|
-
# consistency between different server classes.
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
class OriginURL
|
|
26
|
-
|
|
27
|
-
# Original URL string supplied by the caller.
|
|
28
|
-
# @return [String]
|
|
29
|
-
attr_reader :url
|
|
30
|
-
|
|
31
|
-
# Parsed URI used for origin validation.
|
|
32
|
-
# @return [URI::Generic]
|
|
33
|
-
attr_reader :uri
|
|
34
|
-
|
|
35
|
-
# Parses a candidate server origin; call {#valid?} to check origin restrictions.
|
|
36
|
-
#
|
|
37
|
-
# @param url [String] origin URL to parse
|
|
38
|
-
# @raise [URI::InvalidURIError] if the input cannot be parsed as a URI
|
|
39
|
-
#
|
|
40
|
-
def initialize(url)
|
|
41
|
-
@url = url
|
|
42
|
-
@uri = URI(url)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Determines if a URL conforms to AT Protocol OAuth "simple origin URL" requirements
|
|
46
|
-
# @return [Boolean] true if the URL is a valid origin URL
|
|
47
|
-
def valid?
|
|
48
|
-
https_scheme? &&
|
|
49
|
-
root_path? &&
|
|
50
|
-
!uri.query &&
|
|
51
|
-
!uri.fragment &&
|
|
52
|
-
!uri.userinfo &&
|
|
53
|
-
(!explicit_port? || uri.port != 443)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
def https_scheme?
|
|
59
|
-
uri.scheme == "https"
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def root_path?
|
|
63
|
-
uri.path.empty? || uri.path == "/"
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def explicit_port?
|
|
67
|
-
url.match?(/:\d+/)
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'json'
|
|
4
|
-
require 'uri'
|
|
5
|
-
|
|
6
|
-
require_relative '../configuration'
|
|
7
|
-
require_relative '../errors'
|
|
8
|
-
require_relative '../http_client'
|
|
9
|
-
require_relative '../server_metadata'
|
|
10
|
-
require_relative 'origin_url'
|
|
11
|
-
|
|
12
|
-
module AuthProto
|
|
13
|
-
module ServerMetadata
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
# This class represents a Resource Server (PDS) and is responsible for
|
|
17
|
-
# validating and managing its metadata. It ensures that the authorization
|
|
18
|
-
# server URLs provided are valid and compliant with expected standards.
|
|
19
|
-
# The class also includes functionality to fetch and parse metadata from a
|
|
20
|
-
# remote URL, raising specific errors for invalid or malformed metadata.
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
class ResourceServer
|
|
24
|
-
|
|
25
|
-
# Authorization server origins; validation requires exactly one entry.
|
|
26
|
-
# @return [Array<String>]
|
|
27
|
-
attr_reader :authorization_servers
|
|
28
|
-
|
|
29
|
-
# Validates resource server metadata and its single authorization server origin.
|
|
30
|
-
#
|
|
31
|
-
# @param metadata [Hash{String => Object}] discovery document containing `authorization_servers`
|
|
32
|
-
# @raise [InvalidAuthorizationServer] if the list is missing or does not contain one valid origin
|
|
33
|
-
# @raise [URI::InvalidURIError] if the authorization server URI cannot be parsed
|
|
34
|
-
#
|
|
35
|
-
def initialize(metadata)
|
|
36
|
-
@authorization_servers = validate_authorization_servers(metadata["authorization_servers"])
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Fetches `/.well-known/oauth-protected-resource` using the configured transport.
|
|
40
|
-
#
|
|
41
|
-
# @param url [String] resource server/PDS base URL
|
|
42
|
-
# @return [ResourceServer] validated resource server metadata
|
|
43
|
-
# @raise [InvalidAuthorizationServer] if HTTP transport, JSON parsing or metadata validation fails
|
|
44
|
-
# @raise [HTTPClient::SSRFError] if the transport rejects the initial destination
|
|
45
|
-
# @raise [URI::InvalidURIError] if the PDS or authorization server URL cannot be parsed
|
|
46
|
-
#
|
|
47
|
-
def self.from_url(url)
|
|
48
|
-
response = fetch_metadata(url)
|
|
49
|
-
new(parse_metadata(response[:body]))
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
def validate_authorization_servers(servers)
|
|
56
|
-
ensure_servers_exist(servers)
|
|
57
|
-
ensure_exactly_one_server(servers)
|
|
58
|
-
validate_server_url_format(servers.first)
|
|
59
|
-
servers
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def ensure_servers_exist(servers)
|
|
63
|
-
return if servers.is_a?(Array)
|
|
64
|
-
|
|
65
|
-
raise InvalidAuthorizationServer, "authorization_servers missing"
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def ensure_exactly_one_server(servers)
|
|
69
|
-
return if servers.size == 1
|
|
70
|
-
|
|
71
|
-
raise InvalidAuthorizationServer, "must have exactly one authorization server"
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def validate_server_url_format(server_url)
|
|
75
|
-
return if OriginURL.new(server_url).valid?
|
|
76
|
-
|
|
77
|
-
raise InvalidAuthorizationServer, "invalid authorization server URL format for #{server_url}"
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
class << self
|
|
81
|
-
private
|
|
82
|
-
|
|
83
|
-
def fetch_metadata(url)
|
|
84
|
-
metadata_url = URI.join(url, "/.well-known/oauth-protected-resource")
|
|
85
|
-
AuthProto.configuration.http_client.get(metadata_url.to_s)
|
|
86
|
-
rescue HTTPClient::HTTPError => e
|
|
87
|
-
raise InvalidAuthorizationServer, "Failed to fetch resource server metadata: #{e.message}"
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def parse_metadata(body)
|
|
91
|
-
JSON.parse(body)
|
|
92
|
-
rescue JSON::ParserError => e
|
|
93
|
-
raise InvalidAuthorizationServer, "Invalid JSON in resource server metadata: #{e.message}"
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
module AuthProto
|
|
2
|
-
# Handles and validates AT Protocol OAuth client metadata.
|
|
3
|
-
class ClientMetadata
|
|
4
|
-
AUTH_PRIVATE_KEY_JWT: String
|
|
5
|
-
SCOPE_PATTERN: Regexp
|
|
6
|
-
|
|
7
|
-
@client_id: String
|
|
8
|
-
|
|
9
|
-
@grant_types: Array[String]
|
|
10
|
-
|
|
11
|
-
@response_types: Array[String]
|
|
12
|
-
|
|
13
|
-
@redirect_uris: Array[String]
|
|
14
|
-
|
|
15
|
-
@scope: String
|
|
16
|
-
|
|
17
|
-
@client_name: String?
|
|
18
|
-
|
|
19
|
-
@application_type: String
|
|
20
|
-
|
|
21
|
-
@client_uri: String?
|
|
22
|
-
|
|
23
|
-
@logo_uri: String?
|
|
24
|
-
|
|
25
|
-
@tos_uri: String?
|
|
26
|
-
|
|
27
|
-
@policy_uri: String?
|
|
28
|
-
|
|
29
|
-
@token_endpoint_auth_method: String
|
|
30
|
-
|
|
31
|
-
@token_endpoint_auth_signing_alg: String?
|
|
32
|
-
|
|
33
|
-
@jwks: Hash[String, untyped]?
|
|
34
|
-
|
|
35
|
-
@jwks_uri: String?
|
|
36
|
-
|
|
37
|
-
attr_reader client_id: String
|
|
38
|
-
attr_reader grant_types: Array[String]
|
|
39
|
-
attr_reader response_types: Array[String]
|
|
40
|
-
attr_reader redirect_uris: Array[String]
|
|
41
|
-
attr_reader scope: String
|
|
42
|
-
attr_reader client_name: String?
|
|
43
|
-
attr_reader client_uri: String?
|
|
44
|
-
attr_reader logo_uri: String?
|
|
45
|
-
attr_reader tos_uri: String?
|
|
46
|
-
attr_reader policy_uri: String?
|
|
47
|
-
attr_reader token_endpoint_auth_method: String
|
|
48
|
-
attr_reader jwks: Hash[String, untyped]?
|
|
49
|
-
attr_reader jwks_uri: String?
|
|
50
|
-
attr_reader application_type: String
|
|
51
|
-
|
|
52
|
-
def initialize: (Hash[String, untyped] metadata) -> void
|
|
53
|
-
|
|
54
|
-
def self.from_url: (String url) -> ClientMetadata
|
|
55
|
-
|
|
56
|
-
def confidential?: () -> bool
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
def validate_and_set_metadata: (Hash[String, untyped] metadata) -> void
|
|
61
|
-
|
|
62
|
-
def validate_client_id: (String client_id) -> String
|
|
63
|
-
|
|
64
|
-
def valid_https_client_id?: (String client_id) -> bool
|
|
65
|
-
|
|
66
|
-
def valid_localhost_client_id?: (String client_id) -> bool
|
|
67
|
-
|
|
68
|
-
def validate_grant_types: (Array[String] grant_types) -> Array[String]
|
|
69
|
-
|
|
70
|
-
def validate_response_types: (Array[String] response_types) -> Array[String]
|
|
71
|
-
|
|
72
|
-
def validate_redirect_uris: (Array[String] uris) -> Array[String]
|
|
73
|
-
|
|
74
|
-
def validate_redirect_uri: (String value) -> void
|
|
75
|
-
|
|
76
|
-
def validate_https_redirect_uri: (URI::Generic uri, String? port) -> void
|
|
77
|
-
|
|
78
|
-
def validate_redirect_uri_origin: (URI::Generic uri) -> void
|
|
79
|
-
|
|
80
|
-
def validate_native_redirect_uri: (URI::Generic uri, String? port) -> void
|
|
81
|
-
|
|
82
|
-
def validate_custom_scheme: (URI::Generic uri) -> void
|
|
83
|
-
|
|
84
|
-
def validate_scope: (String scope) -> String
|
|
85
|
-
|
|
86
|
-
def validate_application_type: (String? type) -> String
|
|
87
|
-
|
|
88
|
-
def validate_client_name: (String? name) -> String?
|
|
89
|
-
|
|
90
|
-
def validate_client_uri: (String? uri) -> String?
|
|
91
|
-
|
|
92
|
-
def validate_https_uri: (String? uri) -> String?
|
|
93
|
-
|
|
94
|
-
def validate_jwks: (Hash[String, untyped] jwks) -> void
|
|
95
|
-
|
|
96
|
-
def validate_auth_methods: (Hash[String, untyped] metadata) -> void
|
|
97
|
-
|
|
98
|
-
def validate_auth_public: (Hash[String, untyped] metadata) -> void
|
|
99
|
-
|
|
100
|
-
def validate_auth_confidential: (Hash[String, untyped] metadata) -> void
|
|
101
|
-
|
|
102
|
-
def validate_dpop: (Hash[String, untyped] metadata) -> void
|
|
103
|
-
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
module AuthProto
|
|
108
|
-
module ApplicationType
|
|
109
|
-
WEB: String
|
|
110
|
-
NATIVE: String
|
|
111
|
-
ALL: Array[String]
|
|
112
|
-
end
|
|
113
|
-
end
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
module AuthProto
|
|
2
|
-
module ServerMetadata
|
|
3
|
-
class OriginURL
|
|
4
|
-
@url: String
|
|
5
|
-
@uri: URI::Generic
|
|
6
|
-
|
|
7
|
-
attr_reader url: String
|
|
8
|
-
attr_reader uri: URI::Generic
|
|
9
|
-
|
|
10
|
-
def initialize: (String url) -> void
|
|
11
|
-
|
|
12
|
-
def valid?: () -> bool
|
|
13
|
-
|
|
14
|
-
private
|
|
15
|
-
|
|
16
|
-
def https_scheme?: () -> bool
|
|
17
|
-
def root_path?: () -> bool
|
|
18
|
-
def explicit_port?: () -> bool
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
module AuthProto
|
|
2
|
-
module ServerMetadata
|
|
3
|
-
class ResourceServer
|
|
4
|
-
@authorization_servers: Array[String]
|
|
5
|
-
|
|
6
|
-
attr_reader authorization_servers: Array[String]
|
|
7
|
-
|
|
8
|
-
def initialize: (Hash[String, untyped] metadata) -> void
|
|
9
|
-
|
|
10
|
-
def self.from_url: (String url) -> ResourceServer
|
|
11
|
-
|
|
12
|
-
private
|
|
13
|
-
|
|
14
|
-
def validate_authorization_servers: (Array[String] servers) -> Array[String]
|
|
15
|
-
|
|
16
|
-
def ensure_servers_exist: (Array[String] | nil) -> void
|
|
17
|
-
|
|
18
|
-
def ensure_exactly_one_server: (Array[String]) -> void
|
|
19
|
-
|
|
20
|
-
def validate_server_url_format: (String server_url) -> void
|
|
21
|
-
|
|
22
|
-
private def self.fetch_metadata: (String url) -> http_response
|
|
23
|
-
|
|
24
|
-
private def self.parse_metadata: (String body) -> Hash[String, untyped]
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|