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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/AGENTS.md +80 -0
  4. data/CHANGELOG.md +48 -7
  5. data/README.md +20 -0
  6. data/examples/confidential_client/.gitignore +2 -1
  7. data/examples/confidential_client/Gemfile.lock +5 -1
  8. data/examples/confidential_client/README.md +2 -2
  9. data/examples/confidential_client/app.rb +17 -17
  10. data/examples/confidential_client/config/{client-metadata.example.json → oauth-client-metadata.example.json} +0 -1
  11. data/examples/confidential_client/scripts/generate_keys.rb +15 -9
  12. data/examples/minisky_console/Gemfile.lock +2 -2
  13. data/examples/minisky_console/localhost.rb +8 -25
  14. data/examples/minisky_console/production.rb +1 -1
  15. data/lib/authproto/client.rb +150 -201
  16. data/lib/authproto/dpop/client.rb +12 -60
  17. data/lib/authproto/dpop/key_manager.rb +17 -17
  18. data/lib/authproto/dpop/nonce_manager.rb +5 -19
  19. data/lib/authproto/dpop/proof_generator.rb +27 -23
  20. data/lib/authproto/errors.rb +1 -1
  21. data/lib/authproto/http_client.rb +17 -31
  22. data/lib/authproto/localhost_server.rb +15 -6
  23. data/lib/authproto/metadata/authorization_server.rb +358 -0
  24. data/lib/authproto/metadata/client_metadata.rb +574 -0
  25. data/lib/authproto/metadata/resource_server.rb +137 -0
  26. data/lib/authproto/{server_metadata.rb → metadata.rb} +7 -7
  27. data/lib/authproto/minisky.rb +8 -10
  28. data/lib/authproto/par/client.rb +51 -55
  29. data/lib/authproto/par/client_assertion.rb +58 -15
  30. data/lib/authproto/par/request.rb +58 -140
  31. data/lib/authproto/par/response.rb +9 -10
  32. data/lib/authproto/par.rb +3 -5
  33. data/lib/authproto/pkce.rb +29 -106
  34. data/lib/authproto/serialization/session.rb +11 -11
  35. data/lib/authproto/serialization/token_set.rb +6 -6
  36. data/lib/authproto/state/session.rb +27 -31
  37. data/lib/authproto/state/session_manager.rb +2 -2
  38. data/lib/authproto/state/token_set.rb +4 -4
  39. data/lib/authproto/state.rb +3 -3
  40. data/lib/authproto/token/refresh.rb +35 -43
  41. data/lib/authproto/validated_uri.rb +57 -0
  42. data/lib/authproto/version.rb +1 -1
  43. data/lib/authproto.rb +1 -0
  44. data/misc/yard_attributes.rb +34 -0
  45. data/sig/authproto/client.rbs +9 -10
  46. data/sig/authproto/dpop/client.rbs +1 -8
  47. data/sig/authproto/dpop/nonce_manager.rbs +0 -1
  48. data/sig/authproto/dpop/proof_generator.rbs +5 -5
  49. data/sig/authproto/http_client.rbs +2 -2
  50. data/sig/authproto/{server_metadata → metadata}/authorization_server.rbs +18 -12
  51. data/sig/authproto/metadata/client_metadata.rbs +118 -0
  52. data/sig/authproto/metadata/resource_server.rbs +27 -0
  53. data/sig/authproto/minisky_oauth.rbs +5 -3
  54. data/sig/authproto/par/client.rbs +4 -6
  55. data/sig/authproto/par/client_assertion.rbs +2 -1
  56. data/sig/authproto/par/request.rbs +5 -19
  57. data/sig/authproto/par/response.rbs +1 -5
  58. data/sig/authproto/pkce.rbs +6 -16
  59. data/sig/authproto/serialization/session.rbs +2 -2
  60. data/sig/authproto/state/session.rbs +4 -7
  61. data/sig/authproto/state/session_manager.rbs +2 -2
  62. data/sig/authproto/token/refresh.rbs +4 -4
  63. data/sig/authproto/validated_uri.rbs +36 -0
  64. data/sig/authproto.rbs +5 -3
  65. metadata +13 -11
  66. data/lib/authproto/client_metadata.rb +0 -478
  67. data/lib/authproto/server_metadata/authorization_server.rb +0 -267
  68. data/lib/authproto/server_metadata/origin_url.rb +0 -71
  69. data/lib/authproto/server_metadata/resource_server.rb +0 -98
  70. data/sig/authproto/client_metadata.rbs +0 -113
  71. data/sig/authproto/server_metadata/origin_url.rbs +0 -21
  72. data/sig/authproto/server_metadata/resource_server.rbs +0 -27
@@ -52,9 +52,9 @@ module AuthProto
52
52
  refresh_token: nil
53
53
  )
54
54
  validate_token_type(token_type)
55
- validate_required("access_token", access_token)
56
- validate_required("scope", scope)
57
- validate_required("sub", sub)
55
+ validate_required('access_token', access_token)
56
+ validate_required('scope', scope)
57
+ validate_required('sub', sub)
58
58
  validate_expires_in(expires_in)
59
59
 
60
60
  @access_token = access_token
@@ -84,7 +84,7 @@ module AuthProto
84
84
  private
85
85
 
86
86
  def validate_token_type(type)
87
- raise ArgumentError, "token_type must be DPoP" unless type == "DPoP"
87
+ raise ArgumentError, "token_type must be DPoP" unless type == 'DPoP'
88
88
  end
89
89
 
90
90
  def validate_required(name, value)
@@ -29,15 +29,15 @@ module AuthProto
29
29
  # # Create a new session
30
30
  # session = manager.create_session(
31
31
  # client_id: "https://myapp.com/oauth-client-metadata.json",
32
- # scope: "atproto"
32
+ # scope: 'atproto'
33
33
  # )
34
34
  #
35
35
  # # Update session with tokens
36
36
  # tokens = AuthProto::State::TokenSet.new(
37
37
  # access_token: "...",
38
- # token_type: "DPoP",
38
+ # token_type: 'DPoP',
39
39
  # expires_in: 3600,
40
- # scope: "atproto",
40
+ # scope: 'atproto',
41
41
  # sub: "did:plc:abcdef123"
42
42
  # )
43
43
  # session.tokens = tokens
@@ -65,7 +65,7 @@ module AuthProto
65
65
  super(
66
66
  "Token has expired",
67
67
  token_type: token_type,
68
- error_code: "token_expired",
68
+ error_code: 'token_expired',
69
69
  retry_possible: true
70
70
  )
71
71
  end
@@ -84,7 +84,7 @@ module AuthProto
84
84
  super(
85
85
  "Token is invalid",
86
86
  token_type: token_type,
87
- error_code: "token_invalid",
87
+ error_code: 'token_invalid',
88
88
  retry_possible: false
89
89
  )
90
90
  end
@@ -104,8 +104,8 @@ module AuthProto
104
104
  def initialize(message, retry_possible: true)
105
105
  super(
106
106
  message,
107
- token_type: "refresh",
108
- error_code: "refresh_failed",
107
+ token_type: 'refresh',
108
+ error_code: 'refresh_failed',
109
109
  retry_possible: retry_possible
110
110
  )
111
111
  end
@@ -139,27 +139,30 @@ module AuthProto
139
139
  attr_reader :dpop_client
140
140
 
141
141
  # Server providing the token endpoint.
142
- # @return [ServerMetadata::AuthorizationServer]
142
+ # @return [Metadata::AuthorizationServer]
143
143
  attr_reader :auth_server
144
144
 
145
145
  # Client metadata used for optional confidential-client assertions.
146
- # @return [ClientMetadata]
146
+ # @return [Metadata::ClientMetadata]
147
147
  attr_reader :client_metadata
148
148
 
149
149
  # Creates a synchronized refresh operation for an existing OAuth session.
150
150
  #
151
151
  # @param session [State::Session] session with an access and refresh token
152
152
  # @param dpop_client [DPoP::Client] proof signer and nonce tracker
153
- # @param auth_server [ServerMetadata::AuthorizationServer] server with a token endpoint
154
- # @param client_metadata [ClientMetadata] client ID and authentication key configuration
153
+ # @param auth_server [Metadata::AuthorizationServer] server with a token endpoint
154
+ # @param client_metadata [Metadata::ClientMetadata] client ID and authentication key configuration
155
+ # @param signing_key [Hash, JOSE::JWK, nil] private ES256 client-authentication key
155
156
  #
156
- def initialize(session:, dpop_client:, auth_server:, client_metadata:)
157
+ def initialize(session:, dpop_client:, auth_server:, client_metadata:, signing_key: nil)
157
158
  super() # Initialize MonitorMixin
159
+
158
160
  @session = session
159
161
  @dpop_client = dpop_client
160
162
  @auth_server = auth_server
161
163
  @attempt_count = 0
162
164
  @client_metadata = client_metadata
165
+ @signing_key = signing_key
163
166
  end
164
167
 
165
168
  # Exchanges a refresh token for a replacement token set.
@@ -171,7 +174,7 @@ module AuthProto
171
174
  # @return [State::TokenSet] validated replacement tokens
172
175
  # @raise [RefreshError] if the session is not renewable, refresh fails, or retries are exhausted
173
176
  #
174
- def perform
177
+ def perform_refresh
175
178
  synchronize do
176
179
  raise RefreshError.new("No refresh token available", retry_possible: false) unless session.renewable?
177
180
  raise RefreshError.new("No access token to refresh", retry_possible: false) if session.tokens.nil?
@@ -203,7 +206,6 @@ module AuthProto
203
206
  end
204
207
  end
205
208
 
206
- # Reached max retries
207
209
  raise RefreshError.new(
208
210
  "Token refresh failed after #{MAX_RETRIES} attempts: #{last_error.message}",
209
211
  retry_possible: false
@@ -234,20 +236,15 @@ module AuthProto
234
236
  end
235
237
 
236
238
  def make_token_request(session)
237
- # Generate proof
238
- proof = dpop_client.generate_proof(
239
- http_method: "POST",
240
- http_uri: auth_server.token_endpoint
241
- )
239
+ dpop_proof = dpop_client.generate_proof(method: 'POST', uri: auth_server.token_endpoint)
242
240
 
243
241
  body = {
244
- grant_type: "refresh_token",
242
+ grant_type: 'refresh_token',
245
243
  refresh_token: session.tokens.refresh_token,
246
244
  scope: session.scope,
247
245
  client_id: client_metadata.client_id
248
246
  }
249
247
 
250
- # Add client authentication if available
251
248
  add_client_authentication(body) if client_metadata.confidential?
252
249
 
253
250
  AuthProto.configuration.http_client.post(
@@ -255,7 +252,7 @@ module AuthProto
255
252
  body: body,
256
253
  headers: {
257
254
  "Content-Type" => "application/x-www-form-urlencoded",
258
- "DPoP" => proof
255
+ "DPoP" => dpop_proof
259
256
  }
260
257
  )
261
258
  end
@@ -265,13 +262,13 @@ module AuthProto
265
262
  return false if response[:body].nil?
266
263
 
267
264
  error_data = JSON.parse(response[:body])
268
- error_data.is_a?(Hash) && error_data["error"] == "use_dpop_nonce"
265
+ error_data.is_a?(Hash) && error_data['error'] == DPoP::ERROR_USE_NONCE
269
266
  rescue JSON::ParserError
270
267
  false
271
268
  end
272
269
 
273
270
  def extract_dpop_nonce(response)
274
- nonce = (response[:headers] || {}).detect { |k, v| k.downcase == "dpop-nonce" }&.last
271
+ nonce = (response[:headers] || {}).detect { |k, v| k.downcase == 'dpop-nonce' }&.last
275
272
 
276
273
  unless nonce.is_a?(String) && !nonce.empty?
277
274
  raise RefreshError.new("No DPoP nonce provided in error response", retry_possible: false)
@@ -281,19 +278,14 @@ module AuthProto
281
278
  end
282
279
 
283
280
  def add_client_authentication(body)
284
- return unless client_metadata.jwks && !client_metadata.jwks["keys"].empty?
285
-
286
- signing_key = JOSE::JWK.from_map(client_metadata.jwks["keys"].first)
287
281
  client_assertion = PAR::ClientAssertion.new(
288
282
  client_id: client_metadata.client_id,
289
- signing_key: signing_key
283
+ signing_key: @signing_key
290
284
  )
291
285
 
292
286
  body.merge!(
293
287
  client_assertion_type: PAR::CLIENT_ASSERTION_TYPE,
294
- client_assertion: client_assertion.generate_jwt(
295
- audience: auth_server.issuer
296
- )
288
+ client_assertion: client_assertion.generate_jwt(audience: auth_server.issuer)
297
289
  )
298
290
  end
299
291
 
@@ -319,12 +311,12 @@ module AuthProto
319
311
  validate_refresh_response(data)
320
312
 
321
313
  AuthProto::State::TokenSet.new(
322
- access_token: data["access_token"],
323
- token_type: data["token_type"],
324
- expires_in: data["expires_in"],
325
- refresh_token: data["refresh_token"],
326
- scope: data["scope"],
327
- sub: data["sub"]
314
+ access_token: data['access_token'],
315
+ token_type: data['token_type'],
316
+ expires_in: data['expires_in'],
317
+ refresh_token: data['refresh_token'],
318
+ scope: data['scope'],
319
+ sub: data['sub']
328
320
  )
329
321
  rescue JSON::ParserError => e
330
322
  raise RefreshError.new("Invalid response format: #{e.message}", retry_possible: false)
@@ -339,10 +331,10 @@ module AuthProto
339
331
  raise RefreshError.new("Invalid error response format", retry_possible: false)
340
332
  end
341
333
 
342
- error_description = error_data["error_description"] || error_data["error"]
334
+ error_description = error_data['error_description'] || error_data['error']
343
335
 
344
- case error_data["error"]
345
- when "use_dpop_nonce"
336
+ case error_data['error']
337
+ when DPoP::ERROR_USE_NONCE
346
338
  extract_dpop_nonce(response)
347
339
  dpop_client.process_response(response[:headers], auth_server.issuer)
348
340
  raise RefreshError.new("Retry with DPoP nonce", retry_possible: true)
@@ -384,28 +376,28 @@ module AuthProto
384
376
  end
385
377
  end
386
378
 
387
- unless data["expires_in"].is_a?(Integer)
379
+ unless data['expires_in'].is_a?(Integer)
388
380
  raise RefreshError.new("Invalid expires_in in response", retry_possible: false)
389
381
  end
390
382
 
391
- unless data["refresh_token"].nil? || data["refresh_token"].is_a?(String)
383
+ unless data['refresh_token'].nil? || data['refresh_token'].is_a?(String)
392
384
  raise RefreshError.new("Invalid refresh_token in response", retry_possible: false)
393
385
  end
394
386
 
395
387
  # Token type must be DPoP
396
- unless data["token_type"] == "DPoP"
397
- raise RefreshError.new("Invalid token_type: #{data["token_type"]}", retry_possible: false)
388
+ unless data['token_type'] == 'DPoP'
389
+ raise RefreshError.new("Invalid token_type: #{data['token_type']}", retry_possible: false)
398
390
  end
399
391
 
400
392
  # Scope must include original scopes
401
393
  original_scopes = session.scope.split
402
- response_scopes = data["scope"].split
394
+ response_scopes = data['scope'].split
403
395
  unless (original_scopes - response_scopes).empty?
404
396
  raise RefreshError.new("Invalid scope in response", retry_possible: false)
405
397
  end
406
398
 
407
399
  # Subject must match
408
- return if data["sub"] == session.tokens.sub
400
+ return if data['sub'] == session.tokens.sub
409
401
 
410
402
  raise RefreshError.new("Subject mismatch in response", retry_possible: false)
411
403
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'uri'
5
+
6
+ module AuthProto
7
+
8
+ #
9
+ # An HTTP(S) URI with a required host and preserved source components.
10
+ #
11
+ # Unlike URI::HTTP, port is the explicit port string (or nil when omitted),
12
+ # userinfo preserves an empty component, and path treats "/" as empty.
13
+ # Other readers and string conversion delegate to the parsed URI.
14
+ #
15
+
16
+ class ValidatedURI
17
+ extend Forwardable
18
+
19
+ attr_reader :port, :userinfo, :path
20
+
21
+ def_delegators :@uri, :scheme, :host, :hostname, :query, :fragment,
22
+ :user, :password, :registry, :opaque, :default_port,
23
+ :request_uri, :origin, :absolute?, :relative?, :hierarchical?,
24
+ :to_s, :to_str
25
+
26
+ # @param url [String, URI::Generic] URL to parse; URI inputs use their serialized form
27
+ # @raise [URI::InvalidURIError] if the URL is malformed, not HTTP(S), or has no host
28
+ #
29
+ def initialize(url)
30
+ unless url.is_a?(String) || url.is_a?(URI::Generic)
31
+ raise URI::InvalidURIError, "URL must be a string or URI"
32
+ end
33
+
34
+ source = url.to_s
35
+ @uri = URI.parse(source)
36
+
37
+ unless @uri.is_a?(URI::HTTP) && @uri.host && !@uri.host.empty?
38
+ raise URI::InvalidURIError, "URL must use HTTP or HTTPS and include a host"
39
+ end
40
+
41
+ @userinfo, @port = URI.split(source).values_at(1, 3)
42
+ @path = (@uri.path == '/') ? '' : @uri.path
43
+ end
44
+
45
+ def specified_port
46
+ port
47
+ end
48
+
49
+ def effective_port
50
+ @uri.port
51
+ end
52
+
53
+ def to_uri
54
+ @uri
55
+ end
56
+ end
57
+ end
@@ -4,5 +4,5 @@ module AuthProto
4
4
 
5
5
  # Current gem version.
6
6
  # @return [String]
7
- VERSION = "0.3.0.beta1"
7
+ VERSION = "0.3.0.beta2"
8
8
  end
data/lib/authproto.rb CHANGED
@@ -6,6 +6,7 @@ require_relative 'authproto/http_client'
6
6
  require_relative 'authproto/insecure_http_client'
7
7
  require_relative 'authproto/localhost_server'
8
8
  require_relative 'authproto/serialization/dpop_key'
9
+ require_relative 'authproto/validated_uri'
9
10
  require_relative 'authproto/version'
10
11
 
11
12
  #
@@ -0,0 +1,34 @@
1
+ require 'yard'
2
+
3
+ YARD::Parser::SourceParser.after_parse_list do
4
+ fields = YARD::Registry.at('AuthProto::PAR::Request::Fields').attributes[:instance]
5
+
6
+ {
7
+ 'AuthProto::PAR::Request' => false,
8
+ 'AuthProto::PAR::Request::Configuration' => true
9
+ }.each do |path, writable|
10
+ target = YARD::Registry.at(path)
11
+
12
+ fields.each do |name, attribute|
13
+ original = attribute[:read]
14
+
15
+ reader = YARD::CodeObjects::MethodObject.new(target, name, :instance)
16
+ reader.docstring = original.docstring.to_raw
17
+ reader.signature = "def #{name}"
18
+
19
+ target.attributes[:instance][name] = { read: reader, write: nil }
20
+
21
+ next unless writable
22
+
23
+ return_tag = reader.tag(:return)
24
+ return_tag.types = (return_tag.types + ['nil']).uniq
25
+
26
+ writer = YARD::CodeObjects::MethodObject.new(target, "#{name}=", :instance)
27
+ writer.docstring = original.docstring.to_raw
28
+ writer.parameters = [['value', nil]]
29
+ writer.signature = "def #{name}=(value)"
30
+
31
+ target.attributes[:instance][name][:write] = writer
32
+ end
33
+ end
34
+ end
@@ -11,29 +11,28 @@ module AuthProto
11
11
 
12
12
  attr_reader client_id: String
13
13
  attr_reader redirect_uri: String
14
- attr_reader client_metadata: ClientMetadata
14
+ attr_reader client_metadata: Metadata::ClientMetadata
15
15
  attr_reader session_manager: State::SessionManager
16
16
  attr_reader dpop_client: DPoP::Client
17
17
 
18
- def initialize: (client_id: String, redirect_uri: String, ?metadata: json_object?, ?dpop_key: (json_object | JOSE::Map)?) -> void
19
- def authorize: (?handle: String?, ?pds_url: String?, ?scope: String) -> authorization_result
18
+ def initialize: (client_id: String, redirect_uri: String, ?metadata: json_object?, ?dpop_key: (json_object | JOSE::Map)?, ?signing_key: (json_object | JOSE::JWK)?) -> void
19
+ def authorize: (?handle: String?, ?pds: String?, ?scope: String) -> authorization_result
20
20
  def handle_callback: (code: String, state: String, iss: String) -> callback_result
21
21
  def get_tokens: (String session_id) -> token_info?
22
22
  def refresh_token: (String session_id) -> refreshed_tokens
23
23
  def authorized?: (String session_id) -> bool
24
24
  def auth_headers: (session_id: String, method: String, url: String) -> Hash[String, String]
25
- def remove_session: (String session_id) -> void
25
+ def delete_session: (String session_id) -> void
26
26
  def cleanup_expired_sessions: () -> void
27
27
 
28
28
  private
29
- def load_client_metadata: (json_object? metadata) -> ClientMetadata
29
+ def load_client_metadata: (json_object? metadata) -> Metadata::ClientMetadata
30
30
  def validate_redirect_uri: () -> void
31
+ def initialize_signing_key: ((json_object | JOSE::JWK)? key) -> JOSE::JWK?
31
32
  def initialize_dpop: ((json_object | JOSE::Map)? key) -> DPoP::Client
32
- def validate_auth_params!: (String? handle, String? pds_url, String scope) -> void
33
- def resolve_from_handle: (String handle, State::Session session) -> { server: ServerMetadata::AuthorizationServer, pds: String }
34
- def resolve_from_pds: (String pds_url, State::Session session) -> { server: ServerMetadata::AuthorizationServer, pds: String }
35
- def resolve_auth_server: (String pds_url) -> ServerMetadata::AuthorizationServer
36
- def generate_authorization_url: (ServerMetadata::AuthorizationServer auth_server, State::Session session, ?login_hint: String?) -> String
33
+ def validate_auth_params: (String? handle, String? pds, String scope) -> void
34
+ def resolve_auth_server: (String pds) -> Metadata::AuthorizationServer
35
+ def submit_authorization_par: (Metadata::AuthorizationServer authorization_server, State::Session session, ?login_hint: String?) -> String
37
36
  def exchange_code: (code: String, session: State::Session) -> json_object
38
37
  def make_token_request: (String code, State::Session session) -> http_response
39
38
  def requires_dpop_nonce?: (http_response response) -> bool
@@ -9,15 +9,8 @@ module AuthProto
9
9
  attr_reader nonce_manager: NonceManager
10
10
 
11
11
  def initialize: (?key_manager: KeyManager?, ?nonce_ttl: Integer?) -> void
12
- def generate_proof: (http_method: String, http_uri: String, ?access_token: String?, ?nonce: String?) -> JOSE::SignedBinary
12
+ def generate_proof: (method: String, uri: String, ?access_token: String?, ?nonce: String?) -> JOSE::SignedBinary
13
13
  def process_response: (Hash[String, String]? response_headers, String server_url) -> void
14
- def request_headers: (String proof) -> { "DPoP" => String }
15
- def public_key: () -> json_object
16
- def export_key: (?include_private: bool) -> JOSE::Map
17
-
18
- private
19
- def extract_nonce: (Hash[String | Symbol, String] headers) -> String?
20
- def origin_for_uri: (URI::HTTP uri) -> String
21
14
  end
22
15
  end
23
16
  end
@@ -12,7 +12,6 @@ module AuthProto
12
12
  def update: (nonce: String, server_url: String) -> void
13
13
  def get: (String server_url) -> String?
14
14
  def clear: (String server_url) -> bool
15
- def valid_nonce?: (String server_url) -> bool
16
15
 
17
16
  private
18
17
  def normalize_server_url: (String url) -> String
@@ -11,8 +11,8 @@ module AuthProto
11
11
  def initialize: (AuthProto::DPoP::KeyManager key_manager) -> void
12
12
 
13
13
  def generate: (
14
- http_method: String,
15
- http_uri: String,
14
+ method: String,
15
+ uri: String,
16
16
  ?nonce: String?,
17
17
  ?access_token: String?,
18
18
  ?ath: bool?
@@ -20,13 +20,13 @@ module AuthProto
20
20
 
21
21
  private
22
22
 
23
- def validate_inputs: (String http_method, String http_uri) -> void
23
+ def validate_inputs: (String method, String uri) -> void
24
24
 
25
25
  def build_header: () -> { typ: "dpop+jwt", alg: "ES256", jwk: Hash[String, untyped] }
26
26
 
27
27
  def build_payload: (
28
- http_method: String,
29
- http_uri: String,
28
+ method: String,
29
+ uri: String,
30
30
  ?nonce: String?,
31
31
  ?access_token: String?,
32
32
  ?include_ath: bool?
@@ -34,10 +34,10 @@ module AuthProto
34
34
  def validate_ip: (URI::HTTP uri) -> void
35
35
  def forbidden_ip?: (IPAddr ip) -> bool
36
36
  def make_request: (URI::HTTP uri, ?Hash[String, String] headers, ?Integer redirect_count, ?follow_redirects: bool) -> Net::HTTPResponse
37
- def make_post_request: (URI::HTTP uri, http_body body, ?Hash[String, String] headers, ?Integer redirect_count) -> Net::HTTPResponse
37
+ def make_post_request: (URI::HTTP uri, http_body body, ?Hash[String, String] headers) -> Net::HTTPResponse
38
38
  def configure_http_client: (Net::HTTP http, ?ssl: bool) -> void
39
39
  def add_security_headers: (Net::HTTPRequest request, Hash[String, String] headers) -> void
40
- def handle_redirect: (original_uri: URI::HTTP, response: Net::HTTPResponse, headers: Hash[String, String], redirect_count: Integer, ?method: Symbol, ?body: http_body) -> Net::HTTPResponse
40
+ def handle_redirect: (original_uri: URI::HTTP, response: Net::HTTPResponse, headers: Hash[String, String], redirect_count: Integer) -> Net::HTTPResponse
41
41
  def validate_response: (Net::HTTPResponse response) -> void
42
42
  def check_success_status: (Net::HTTPResponse response) -> void
43
43
  def check_content_length: (Net::HTTPResponse response) -> void
@@ -1,5 +1,5 @@
1
1
  module AuthProto
2
- module ServerMetadata
2
+ module Metadata
3
3
  class AuthorizationServer
4
4
  @issuer: String
5
5
  @authorization_endpoint: String
@@ -30,10 +30,10 @@ module AuthProto
30
30
  def initialize: (Hash[String, untyped] metadata) -> void
31
31
 
32
32
  # Fetches and validates Authorization Server metadata from an issuer URL
33
- # @param issuer [String] Authorization Server issuer URL
33
+ # @param url [String] Authorization Server issuer URL
34
34
  # @return [AuthorizationServer] new instance with fetched metadata
35
35
  # @raise [InvalidAuthorizationServer] if metadata is invalid
36
- def self.from_issuer: (String issuer) -> AuthorizationServer
36
+ def self.from_url: (String url) -> AuthorizationServer
37
37
 
38
38
  def to_h: () -> Hash[Symbol, untyped]
39
39
 
@@ -45,27 +45,33 @@ module AuthProto
45
45
 
46
46
  def validate_https_url: (String url) -> String
47
47
 
48
- def validate_response_types: (Array[String] types) -> void
48
+ def validate_response_types: () -> void
49
49
 
50
- def validate_grant_types: (Array[String] types) -> void
50
+ def validate_grant_types: () -> void
51
51
 
52
- def validate_code_challenge_methods: (Array[String] methods) -> void
52
+ def validate_code_challenge_methods: () -> void
53
53
 
54
- def validate_token_endpoint_auth_methods: (Array[String] methods) -> void
54
+ def validate_token_endpoint_auth_methods: () -> void
55
55
 
56
- def validate_token_endpoint_auth_signing_algs: (Array[String] algs) -> void
56
+ def validate_token_endpoint_auth_signing_algs: () -> void
57
57
 
58
- def validate_dpop_signing_algs: (Array[String] algs) -> void
58
+ def validate_dpop_signing_algs: () -> void
59
59
 
60
- def validate_scopes: (Array[String] scopes) -> void
60
+ def validate_scopes: () -> void
61
+
62
+ def validate_string_array: (untyped values, String field) -> void
61
63
 
62
64
  def validate_boolean_field: (Hash[String, untyped] metadata, String field, bool required_value) -> void
63
65
 
64
66
  private def self.fetch_metadata: (String issuer) -> http_response
65
67
 
66
- private def self.parse_metadata: (String body) -> Hash[String, untyped]
68
+ private def self.validate_source_url: (String issuer) -> void
69
+
70
+ private def self.validate_response: (http_response response) -> void
71
+
72
+ private def self.parse_metadata: (String? body) -> Hash[String, untyped]
67
73
 
68
- private def self.validate_issuer: (String metadata_issuer, String request_issuer) -> void
74
+ private def self.validate_issuer_matches: (String metadata_issuer, String request_issuer) -> void
69
75
  end
70
76
  end
71
77
  end
@@ -0,0 +1,118 @@
1
+ module AuthProto
2
+ module Metadata
3
+ # Handles and validates AT Protocol OAuth client metadata.
4
+ class ClientMetadata
5
+ AUTH_METHOD_PRIVATE_KEY_JWT: String
6
+ AUTH_METHOD_NONE: String
7
+ SCOPE_PATTERN: Regexp
8
+
9
+ @client_id: String
10
+
11
+ @grant_types: Array[String]
12
+
13
+ @response_types: Array[String]
14
+
15
+ @redirect_uris: Array[String]
16
+
17
+ @scope: String
18
+
19
+ @client_name: String?
20
+
21
+ @application_type: String
22
+
23
+ @client_uri: String?
24
+
25
+ @logo_uri: String?
26
+
27
+ @tos_uri: String?
28
+
29
+ @policy_uri: String?
30
+
31
+ @token_endpoint_auth_method: String
32
+
33
+ @token_endpoint_auth_signing_alg: String?
34
+
35
+ @jwks: Hash[String, untyped]?
36
+
37
+ @jwks_uri: String?
38
+
39
+ attr_reader client_id: String
40
+ attr_reader grant_types: Array[String]
41
+ attr_reader response_types: Array[String]
42
+ attr_reader redirect_uris: Array[String]
43
+ attr_reader scope: String
44
+ attr_reader client_name: String?
45
+ attr_reader client_uri: String?
46
+ attr_reader logo_uri: String?
47
+ attr_reader tos_uri: String?
48
+ attr_reader policy_uri: String?
49
+ attr_reader token_endpoint_auth_method: String
50
+ attr_reader jwks: Hash[String, untyped]?
51
+ attr_reader jwks_uri: String?
52
+ attr_reader application_type: String
53
+
54
+ def initialize: (Hash[String, untyped] metadata) -> void
55
+
56
+ def self.from_url: (String url) -> ClientMetadata
57
+
58
+ def self.from_localhost_url: (String url) -> ClientMetadata
59
+
60
+ def confidential?: () -> bool
61
+
62
+ private
63
+
64
+ def validate_and_set_metadata: (Hash[String, untyped] metadata) -> void
65
+
66
+ def validate_client_id: (String client_id) -> String
67
+
68
+ def valid_https_client_id?: (String client_id) -> bool
69
+
70
+ def valid_localhost_client_id?: (String client_id) -> bool
71
+
72
+ def validate_grant_types: (Array[String] grant_types) -> Array[String]
73
+
74
+ def validate_response_types: (Array[String] response_types) -> Array[String]
75
+
76
+ def validate_redirect_uris: (Array[String] uris) -> Array[String]
77
+
78
+ def validate_redirect_uri: (String value) -> void
79
+
80
+ def validate_https_redirect_uri: (String url) -> void
81
+
82
+ def validate_redirect_uri_origin: (String url) -> void
83
+
84
+ def validate_native_redirect_uri: (String url) -> void
85
+
86
+ def validate_custom_scheme: (URI::Generic uri) -> void
87
+
88
+ def validate_scope: (String scope) -> String
89
+
90
+ def validate_application_type: (String? type) -> String
91
+
92
+ def validate_client_name: (String? name) -> String?
93
+
94
+ def validate_client_uri: (String? uri) -> String?
95
+
96
+ def validate_https_uri: (String? uri) -> String?
97
+
98
+ def validate_jwks: (Hash[String, untyped] jwks) -> void
99
+
100
+ def validate_auth_methods: (Hash[String, untyped] metadata) -> void
101
+
102
+ def validate_auth_public: (Hash[String, untyped] metadata) -> void
103
+
104
+ def validate_auth_confidential: (Hash[String, untyped] metadata) -> void
105
+
106
+ def validate_dpop: (Hash[String, untyped] metadata) -> void
107
+
108
+ end
109
+ end
110
+ end
111
+
112
+ module AuthProto
113
+ module ApplicationType
114
+ WEB: String
115
+ NATIVE: String
116
+ ALL: Array[String]
117
+ end
118
+ end