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
|
@@ -52,9 +52,9 @@ module AuthProto
|
|
|
52
52
|
refresh_token: nil
|
|
53
53
|
)
|
|
54
54
|
validate_token_type(token_type)
|
|
55
|
-
validate_required(
|
|
56
|
-
validate_required(
|
|
57
|
-
validate_required(
|
|
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 ==
|
|
87
|
+
raise ArgumentError, "token_type must be DPoP" unless type == 'DPoP'
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
def validate_required(name, value)
|
data/lib/authproto/state.rb
CHANGED
|
@@ -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:
|
|
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:
|
|
38
|
+
# token_type: 'DPoP',
|
|
39
39
|
# expires_in: 3600,
|
|
40
|
-
# scope:
|
|
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:
|
|
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:
|
|
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:
|
|
108
|
-
error_code:
|
|
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 [
|
|
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 [
|
|
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
|
|
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
|
-
|
|
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:
|
|
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" =>
|
|
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[
|
|
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 ==
|
|
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[
|
|
323
|
-
token_type: data[
|
|
324
|
-
expires_in: data[
|
|
325
|
-
refresh_token: data[
|
|
326
|
-
scope: data[
|
|
327
|
-
sub: data[
|
|
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[
|
|
334
|
+
error_description = error_data['error_description'] || error_data['error']
|
|
343
335
|
|
|
344
|
-
case error_data[
|
|
345
|
-
when
|
|
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[
|
|
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[
|
|
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[
|
|
397
|
-
raise RefreshError.new("Invalid token_type: #{data[
|
|
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[
|
|
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[
|
|
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
|
data/lib/authproto/version.rb
CHANGED
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
|
data/sig/authproto/client.rbs
CHANGED
|
@@ -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?, ?
|
|
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
|
|
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
|
|
33
|
-
def
|
|
34
|
-
def
|
|
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: (
|
|
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
|
-
|
|
15
|
-
|
|
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
|
|
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
|
-
|
|
29
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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: (
|
|
48
|
+
def validate_response_types: () -> void
|
|
49
49
|
|
|
50
|
-
def validate_grant_types: (
|
|
50
|
+
def validate_grant_types: () -> void
|
|
51
51
|
|
|
52
|
-
def validate_code_challenge_methods: (
|
|
52
|
+
def validate_code_challenge_methods: () -> void
|
|
53
53
|
|
|
54
|
-
def validate_token_endpoint_auth_methods: (
|
|
54
|
+
def validate_token_endpoint_auth_methods: () -> void
|
|
55
55
|
|
|
56
|
-
def validate_token_endpoint_auth_signing_algs: (
|
|
56
|
+
def validate_token_endpoint_auth_signing_algs: () -> void
|
|
57
57
|
|
|
58
|
-
def validate_dpop_signing_algs: (
|
|
58
|
+
def validate_dpop_signing_algs: () -> void
|
|
59
59
|
|
|
60
|
-
def validate_scopes: (
|
|
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.
|
|
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.
|
|
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
|