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
@@ -1,116 +1,75 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'cgi'
3
+ require 'uri'
4
4
 
5
5
  require_relative '../par'
6
+ require_relative 'client_assertion'
6
7
 
7
8
  module AuthProto
8
9
  module PAR
9
10
 
10
11
  #
11
- # Represents a pushed authorization request
12
+ # Represents a pushed authorization request.
12
13
  #
13
14
 
14
15
  class Request
15
16
 
16
17
  #
17
- # Configuration for request parameters
18
+ # Shared set of fields between {Request} (a read-only, validated request) and {Configuration} (a draft request
19
+ # with writable fields configured in the block passed to {Request.build}).
18
20
  #
19
21
 
20
- class Configuration
22
+ module Fields
21
23
 
22
24
  # OAuth client identifier.
23
- # @return [String, nil]
24
- attr_accessor :client_id
25
+ # @return [String]
26
+ attr_reader :client_id
25
27
 
26
28
  # Registered OAuth callback URI.
27
- # @return [String, nil]
28
- attr_accessor :redirect_uri
29
+ # @return [String]
30
+ attr_reader :redirect_uri
29
31
 
30
32
  # S256 PKCE challenge.
31
- # @return [String, nil]
32
- attr_accessor :code_challenge
33
-
34
- # PKCE method; must be `S256`.
35
- # @return [String, nil]
36
- attr_accessor :code_challenge_method
33
+ # @return [String]
34
+ attr_reader :code_challenge
37
35
 
38
36
  # Random state token to validate the callback.
39
- # @return [String, nil]
40
- attr_accessor :state
37
+ # @return [String]
38
+ attr_reader :state
41
39
 
42
40
  # Space-separated scopes including `atproto`.
43
- # @return [String, nil]
44
- attr_accessor :scope
41
+ # @return [String]
42
+ attr_reader :scope
45
43
 
46
44
  # Account handle to prefill at the authorization server.
47
45
  # @return [String, nil]
48
- attr_accessor :login_hint
46
+ attr_reader :login_hint
49
47
 
50
48
  # Optional authorization nonce parameter.
51
49
  # @return [String, nil]
52
- attr_accessor :nonce
50
+ attr_reader :nonce
53
51
 
54
52
  # Optional proof value retained on the request; {Client#submit} generates its own HTTP proof.
55
53
  # @return [String, nil]
56
- attr_accessor :dpop_proof
54
+ attr_reader :dpop_proof
57
55
 
58
- # JWT bearer assertion type for confidential clients.
59
- # @return [String, nil]
60
- attr_accessor :client_assertion_type
61
-
62
- # Signed client authentication JWT for confidential clients.
63
- # @return [String, nil]
64
- attr_accessor :client_assertion
56
+ # Client authentication signer; a fresh JWT is generated for each form encoding.
57
+ # @return [ClientAssertion, nil]
58
+ attr_reader :client_assertion
65
59
  end
66
60
 
67
- # OAuth response type; always `code`.
68
- # @return [String]
69
- attr_reader :response_type
70
-
71
- # OAuth client identifier.
72
- # @return [String]
73
- attr_reader :client_id
74
-
75
- # Registered OAuth callback URI.
76
- # @return [String]
77
- attr_reader :redirect_uri
78
-
79
- # S256 PKCE challenge.
80
- # @return [String]
81
- attr_reader :code_challenge
82
-
83
- # PKCE method; must be `S256`.
84
- # @return [String]
85
- attr_reader :code_challenge_method
86
-
87
- # Random state token to validate the callback.
88
- # @return [String]
89
- attr_reader :state
90
-
91
- # Space-separated scopes including `atproto`.
92
- # @return [String]
93
- attr_reader :scope
94
-
95
- # Account handle to prefill at the authorization server.
96
- # @return [String, nil]
97
- attr_reader :login_hint
61
+ #
62
+ # A draft PAR request with writable fields, configured inside the block passed to {Request.build}).
63
+ #
98
64
 
99
- # Optional authorization nonce parameter.
100
- # @return [String, nil]
101
- attr_reader :nonce
65
+ class Configuration
66
+ include Fields
102
67
 
103
- # Optional proof value retained on the request; {Client#submit} generates its own HTTP proof.
104
- # @return [String, nil]
105
- attr_reader :dpop_proof
68
+ send(:attr_writer, *Fields.instance_methods(false))
69
+ end
106
70
 
107
- # JWT bearer assertion type for confidential clients.
108
- # @return [String, nil]
109
- attr_reader :client_assertion_type
71
+ include Fields
110
72
 
111
- # Signed client authentication JWT for confidential clients.
112
- # @return [String, nil]
113
- attr_reader :client_assertion
114
73
 
115
74
  # Builds a validated PAR request by yielding a mutable parameter configuration.
116
75
  #
@@ -132,111 +91,70 @@ module AuthProto
132
91
  # @raise [Error] if required values are missing or authentication/PKCE/scope values are invalid
133
92
  #
134
93
  def initialize(config)
135
- # Required parameters
136
- @response_type = "code" # Always "code" for AT Protocol OAuth
137
- @client_id = config.client_id
138
- @redirect_uri = config.redirect_uri
139
- @code_challenge = config.code_challenge
140
- @code_challenge_method = config.code_challenge_method
141
- @state = config.state
142
- @scope = config.scope
143
-
144
- # Optional parameters
145
- @login_hint = config.login_hint
146
- @nonce = config.nonce
147
- @dpop_proof = config.dpop_proof
148
-
149
- # Client authentication
150
- @client_assertion_type = config.client_assertion_type
151
- @client_assertion = config.client_assertion
94
+ Fields.instance_methods(false).each do |field|
95
+ self.instance_variable_set("@#{field}", config.send(field))
96
+ end
152
97
 
153
98
  validate_request
154
99
  end
155
100
 
156
- # Converts request to form-encoded parameters
101
+ # Converts request to form-encoded parameters, generating a fresh client assertion when configured.
102
+ # @param audience [String, nil] authorization server issuer; required for confidential clients
157
103
  # @return [String] Form-encoded request body
158
- def to_form
159
- encode_params(build_params)
104
+ #
105
+ def to_form(audience: nil)
106
+ URI.encode_www_form(build_params(audience: audience))
160
107
  end
161
108
 
109
+
162
110
  private
163
111
 
164
- def build_params
112
+ def build_params(audience:)
165
113
  params = {
166
- "response_type" => response_type,
114
+ "response_type" => 'code',
167
115
  "client_id" => client_id,
168
116
  "redirect_uri" => redirect_uri,
169
117
  "code_challenge" => code_challenge,
170
- "code_challenge_method" => code_challenge_method,
118
+ "code_challenge_method" => PKCE::CHALLENGE_METHOD_S256,
171
119
  "state" => state,
172
120
  "scope" => scope
173
121
  }
174
122
 
175
- add_optional_params(params)
176
- add_client_auth_params(params)
177
- params
178
- end
123
+ params['login_hint'] = login_hint if login_hint
124
+ params['nonce'] = nonce if nonce
179
125
 
180
- def add_optional_params(params)
181
- params["login_hint"] = login_hint if login_hint
182
- params["nonce"] = nonce if nonce
183
- end
126
+ if client_assertion
127
+ unless audience.is_a?(String) && !audience.empty?
128
+ raise Error, "audience is required with client_assertion"
129
+ end
184
130
 
185
- def add_client_auth_params(params)
186
- return unless client_assertion
131
+ params['client_assertion_type'] = CLIENT_ASSERTION_TYPE
132
+ params['client_assertion'] = client_assertion.generate_jwt(audience: audience)
133
+ end
187
134
 
188
- params["client_assertion_type"] = CLIENT_ASSERTION_TYPE
189
- params["client_assertion"] = client_assertion
135
+ params
190
136
  end
191
137
 
192
138
  def validate_request
193
139
  validate_required_params
194
- validate_response_type
195
- validate_code_challenge_method
196
- validate_scope
197
140
  validate_client_auth
141
+
142
+ raise Error, "atproto scope is required" unless scope.split.include?('atproto')
198
143
  end
199
144
 
200
145
  def validate_required_params
201
- %i[client_id redirect_uri code_challenge code_challenge_method state scope].each do |param|
146
+ %i[client_id redirect_uri code_challenge state scope].each do |param|
202
147
  value = send(param)
203
148
  raise Error, "#{param} is required" if value.nil? || value.empty?
204
149
  end
205
150
  end
206
151
 
207
- def validate_response_type
208
- return if response_type == "code"
209
-
210
- raise Error, "response_type must be 'code'"
211
- end
212
-
213
- def validate_code_challenge_method
214
- return if code_challenge_method == "S256"
215
-
216
- raise Error, "code_challenge_method must be 'S256'"
217
- end
218
-
219
- def validate_scope
220
- scopes = scope.split
221
- raise Error, "atproto scope is required" unless scopes.include?("atproto")
222
- end
223
-
224
152
  def validate_client_auth
225
- # If either auth parameter is present, both must be present and valid
226
- has_assertion = !client_assertion.nil?
227
- has_type = !client_assertion_type.nil?
153
+ return if client_assertion.nil?
228
154
 
229
- return unless has_assertion || has_type
230
- unless client_assertion_type == CLIENT_ASSERTION_TYPE
231
- raise Error, "client_assertion_type must be #{CLIENT_ASSERTION_TYPE}"
155
+ unless client_assertion.is_a?(ClientAssertion)
156
+ raise Error, "client_assertion must be a PAR::ClientAssertion"
232
157
  end
233
-
234
- raise Error, "client_assertion required with client_assertion_type" if client_assertion.nil?
235
- raise Error, "client_assertion_type required with client_assertion" if client_assertion_type.nil?
236
- end
237
-
238
- def encode_params(params)
239
- params.map { |k, v| "#{CGI.escape(k)}=#{CGI.escape(v.to_s)}" }.join("&")
240
158
  end
241
159
  end
242
160
  end
@@ -22,21 +22,20 @@ module AuthProto
22
22
  # Creates and validates a PAR response.
23
23
  #
24
24
  # @param request_uri [String] nonempty request URI issued by the authorization server
25
- # @param expires_in [Integer, String] positive lifetime in seconds; converted with `to_i`
26
- # @raise [Error] if the URI is empty or expiry is not positive
25
+ # @param expires_in [Integer] positive lifetime in seconds
26
+ # @raise [Error] if the URI is not a nonempty string or expiry is not a positive integer
27
27
  #
28
28
  def initialize(request_uri:, expires_in:)
29
29
  @request_uri = request_uri
30
- @expires_in = expires_in.to_i
31
- validate_response
32
- end
33
-
30
+ @expires_in = expires_in
34
31
 
35
- private
32
+ unless @request_uri.is_a?(String) && !@request_uri.empty?
33
+ raise Error, "request_uri must be a nonempty string"
34
+ end
36
35
 
37
- def validate_response
38
- raise Error, "request_uri is required" if request_uri.nil? || request_uri.empty?
39
- raise Error, "expires_in must be positive" unless expires_in.positive?
36
+ unless @expires_in.is_a?(Integer) && @expires_in.positive?
37
+ raise Error, "expires_in must be a positive integer"
38
+ end
40
39
  end
41
40
  end
42
41
  end
data/lib/authproto/par.rb CHANGED
@@ -15,7 +15,7 @@ module AuthProto
15
15
  #
16
16
  # @example Basic PAR request
17
17
  # par = AuthProto::PAR::Client.new(
18
- # endpoint: "https://auth.example.com/par",
18
+ # authorization_server: authorization_server,
19
19
  # dpop_client: AuthProto::DPoP::Client.new
20
20
  # )
21
21
  #
@@ -23,14 +23,12 @@ module AuthProto
23
23
  # config.client_id = "https://app.example.com/oauth-client-metadata.json"
24
24
  # config.redirect_uri = "https://app.example.com/callback"
25
25
  # config.code_challenge = "abc123..."
26
- # config.code_challenge_method = "S256"
27
26
  # config.state = "xyz789..."
28
- # config.scope = "atproto"
27
+ # config.scope = 'atproto'
29
28
  # end
30
29
  #
31
30
  # response = par.submit(request)
32
- # auth_url = par.authorization_url(
33
- # authorize_endpoint: "https://auth.example.com/authorize",
31
+ # auth_url = par.build_authorization_url(
34
32
  # request_uri: response.request_uri,
35
33
  # client_id: request.client_id
36
34
  # )
@@ -9,117 +9,40 @@ require_relative 'errors'
9
9
  module AuthProto
10
10
 
11
11
  #
12
- # Implementation of Proof Key for Code Exchange (PKCE) for OAuth 2.0 / AT Protocol
13
- # as specified in RFC 7636.
14
- #
15
- # This module provides functionality to:
16
- # - Generate cryptographically secure code verifiers
17
- # - Create SHA-256 code challenges from verifiers
18
- # - Verify challenge/verifier pairs
19
- #
20
- # Only the S256 challenge method is supported, as required by AT Protocol OAuth.
12
+ # Generates a matching PKCE verifier and S256 challenge for an OAuth authorization flow.
21
13
  #
22
14
 
23
- module PKCE
15
+ class PKCE
24
16
 
25
- # Error raised for PKCE-related failures
17
+ # Error raised when generating a verifier/challenge pair fails.
26
18
  class Error < AuthProto::Error; end
27
19
 
28
- # Minimum PKCE verifier length in characters.
29
- # @return [Integer]
30
- MIN_VERIFIER_LENGTH = 43
31
-
32
- # Maximum PKCE verifier length in characters.
33
- # @return [Integer]
34
- MAX_VERIFIER_LENGTH = 128
35
-
36
- # Allowed PKCE verifier characters from RFC 7636.
37
- # @return [Regexp]
38
- ALLOWED_VERIFIER_CHARS = /^[A-Za-z0-9\-\._~]+$/
39
-
40
- class << self
41
-
42
- # Generates a cryptographically secure random code verifier
43
- #
44
- # @param length [Integer] Length of verifier to generate
45
- # @return [String] The generated code verifier
46
- # @raise [Error] if length is invalid
47
- #
48
- def generate_verifier(length = MAX_VERIFIER_LENGTH)
49
- validate_verifier_length(length)
50
-
51
- # Generate random bytes and encode as URL-safe base64
52
- random_bytes = SecureRandom.random_bytes(length * 3 / 4)
53
- Base64.urlsafe_encode64(random_bytes, padding: false)[0...length]
54
- rescue StandardError => e
55
- raise Error, "Failed to generate verifier: #{e.message}"
56
- end
57
-
58
- # Creates a code challenge from a verifier using SHA-256
59
- #
60
- # @param verifier [String] The code verifier to create challenge from
61
- # @return [String] Base64URL-encoded SHA-256 hash of the verifier
62
- # @raise [Error] if verifier is invalid or hashing fails
63
- #
64
- def generate_challenge(verifier)
65
- validate_verifier(verifier)
66
-
67
- # Hash with SHA-256 and encode as URL-safe base64
68
- digest = OpenSSL::Digest::SHA256.digest(verifier)
69
- Base64.urlsafe_encode64(digest, padding: false)
70
- rescue StandardError => e
71
- raise Error, "Failed to generate challenge: #{e.message}"
72
- end
73
-
74
- # Verifies that a challenge matches a verifier
75
- #
76
- # @param challenge [String] The code challenge to verify
77
- # @param verifier [String] The code verifier to check against
78
- # @return [Boolean] true if challenge matches verifier
79
- # @raise [Error] if inputs are invalid
80
- #
81
- def verify(challenge, verifier)
82
- # Generate challenge from verifier and compare
83
- calculated = generate_challenge(verifier)
84
- secure_compare(calculated, challenge)
85
- rescue Error
86
- # Re-raise PKCE errors
87
- raise
88
- rescue StandardError => e
89
- raise Error, "Challenge verification failed: #{e.message}"
90
- end
91
-
92
-
93
- private
94
-
95
- def validate_verifier_length(length)
96
- return if length.is_a?(Integer) && length.between?(MIN_VERIFIER_LENGTH, MAX_VERIFIER_LENGTH)
97
-
98
- raise Error, "Verifier length must be between #{MIN_VERIFIER_LENGTH} and #{MAX_VERIFIER_LENGTH}"
99
- end
100
-
101
- def validate_verifier(verifier)
102
- raise Error, "Verifier cannot be nil" if verifier.nil?
103
- raise Error, "Verifier cannot be empty" if verifier.empty?
104
-
105
- length = verifier.length
106
- validate_verifier_length(length)
107
-
108
- return if verifier.match?(ALLOWED_VERIFIER_CHARS)
109
-
110
- raise Error, "Verifier contains invalid characters"
111
- end
112
-
113
- # Constant-time string comparison to prevent timing attacks
114
- def secure_compare(str1, str2)
115
- return false unless str1.bytesize == str2.bytesize
116
-
117
- left = str1.unpack("C*")
118
- right = str2.unpack("C*")
119
- result = 0
120
- left.zip(right) { |x, y| result |= x ^ y }
121
- result.zero?
122
- end
20
+ # Challenge method required by AT Protocol OAuth.
21
+ CHALLENGE_METHOD_S256 = 'S256'
22
+
23
+ # Random bytes providing 256 bits of entropy, encoded as a 43-character verifier without padding.
24
+ VERIFIER_BYTES = 32
25
+
26
+ # Cryptographically random, URL-safe code verifier.
27
+ # @return [String]
28
+ attr_reader :verifier
29
+
30
+ # Base64URL-encoded SHA-256 digest of the verifier, without padding.
31
+ # @return [String]
32
+ attr_reader :challenge
33
+
34
+ # Generates a verifier and its matching S256 challenge.
35
+ #
36
+ # Both strings are frozen so the pair cannot be changed after generation.
37
+ #
38
+ # @raise [Error] if random generation or hashing fails
39
+ #
40
+ def initialize
41
+ @verifier = SecureRandom.urlsafe_base64(VERIFIER_BYTES, false).freeze
42
+ digest = OpenSSL::Digest::SHA256.digest(@verifier)
43
+ @challenge = Base64.urlsafe_encode64(digest, padding: false).freeze
44
+ rescue StandardError => e
45
+ raise Error, "Failed to generate PKCE: #{e.message}"
123
46
  end
124
47
  end
125
48
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../server_metadata/authorization_server'
3
+ require_relative '../metadata/authorization_server'
4
4
  require_relative '../state/session'
5
5
  require_relative 'base'
6
6
  require_relative 'token_set'
@@ -44,19 +44,19 @@ module AuthProto
44
44
 
45
45
  def deserialize_data(data)
46
46
  AuthProto::State::Session.new(
47
- client_id: data["client_id"],
48
- scope: data["scope"],
49
- did: data["did"],
50
- auth_server: deserialize_auth_server(data["auth_server"])
47
+ client_id: data['client_id'],
48
+ scope: data['scope'],
49
+ did: data['did'],
50
+ auth_server: deserialize_auth_server(data['auth_server'])
51
51
  ).tap do |session|
52
52
  # Set readonly attributes
53
- session.instance_variable_set(:@session_id, data["session_id"])
54
- session.instance_variable_set(:@state_token, data["state_token"])
55
- session.instance_variable_set(:@pkce_verifier, data["pkce_verifier"])
56
- session.instance_variable_set(:@pkce_challenge, data["pkce_challenge"])
53
+ session.instance_variable_set(:@session_id, data['session_id'])
54
+ session.instance_variable_set(:@state_token, data['state_token'])
55
+ session.instance_variable_set(:@pkce_verifier, data['pkce_verifier'])
56
+ session.instance_variable_set(:@pkce_challenge, data['pkce_challenge'])
57
57
 
58
58
  # Set tokens if present
59
- session.tokens = deserialize_token_set(data["tokens"]) if data["tokens"]
59
+ session.tokens = deserialize_token_set(data['tokens']) if data['tokens']
60
60
  end
61
61
  end
62
62
 
@@ -81,7 +81,7 @@ module AuthProto
81
81
  def deserialize_auth_server(data)
82
82
  return nil unless data
83
83
 
84
- AuthProto::ServerMetadata::AuthorizationServer.new(data)
84
+ AuthProto::Metadata::AuthorizationServer.new(data)
85
85
  end
86
86
  end
87
87
  end
@@ -41,12 +41,12 @@ module AuthProto
41
41
 
42
42
  def deserialize_data(data)
43
43
  AuthProto::State::TokenSet.new(
44
- access_token: data["access_token"],
45
- refresh_token: data["refresh_token"],
46
- token_type: data["token_type"],
47
- expires_in: (Time.parse(data["expires_at"]) - Time.now).to_i,
48
- scope: data["scope"],
49
- sub: data["sub"]
44
+ access_token: data['access_token'],
45
+ refresh_token: data['refresh_token'],
46
+ token_type: data['token_type'],
47
+ expires_in: (Time.parse(data['expires_at']) - Time.now).to_i,
48
+ scope: data['scope'],
49
+ sub: data['sub']
50
50
  )
51
51
  end
52
52
  end
@@ -41,7 +41,7 @@ module AuthProto
41
41
  attr_reader :pkce_challenge
42
42
 
43
43
  # Bound authorization server, once discovered.
44
- # @return [ServerMetadata::AuthorizationServer, nil]
44
+ # @return [Metadata::AuthorizationServer, nil]
45
45
  attr_reader :auth_server
46
46
 
47
47
  # Bound account DID, once resolved.
@@ -56,7 +56,7 @@ module AuthProto
56
56
  #
57
57
  # @param client_id [String] OAuth client ID
58
58
  # @param scope [String] requested space-separated scopes
59
- # @param auth_server [ServerMetadata::AuthorizationServer, nil] pre-resolved authorization server
59
+ # @param auth_server [Metadata::AuthorizationServer, nil] pre-resolved authorization server
60
60
  # @param did [String, nil] pre-resolved account DID
61
61
  # @raise [PKCE::Error] if PKCE generation fails
62
62
  #
@@ -70,17 +70,17 @@ module AuthProto
70
70
  @auth_server = auth_server
71
71
  @did = did
72
72
 
73
- # Generate PKCE values
74
- @pkce_verifier = PKCE.generate_verifier
75
- @pkce_challenge = PKCE.generate_challenge(@pkce_verifier)
73
+ pkce = PKCE.new
74
+ @pkce_verifier = pkce.verifier
75
+ @pkce_challenge = pkce.challenge
76
76
 
77
77
  @tokens = nil
78
78
  end
79
79
 
80
80
  # Binds this session to an authorization server.
81
81
  #
82
- # @param server [ServerMetadata::AuthorizationServer] resolved authorization server
83
- # @return [ServerMetadata::AuthorizationServer] assigned server when called as a method
82
+ # @param server [Metadata::AuthorizationServer] resolved authorization server
83
+ # @return [Metadata::AuthorizationServer] assigned server when called as a method
84
84
  # @raise [SessionError] if already bound to a server with a different issuer
85
85
  #
86
86
  def authorization_server=(server)
@@ -124,6 +124,26 @@ module AuthProto
124
124
  end
125
125
  end
126
126
 
127
+ # Returns the current tokens and session identifiers, with expiry in seconds from now.
128
+ #
129
+ # @return [Hash{Symbol => String, Integer, nil}, nil] session data, or `nil` before tokens are assigned
130
+ #
131
+ def session_data
132
+ synchronize do
133
+ return nil unless @tokens
134
+
135
+ {
136
+ access_token: @tokens.access_token,
137
+ token_type: @tokens.token_type,
138
+ expires_in: (@tokens.expires_at - Time.now).to_i,
139
+ refresh_token: @tokens.refresh_token,
140
+ scope: @tokens.scope,
141
+ session_id: @session_id,
142
+ did: @did
143
+ }
144
+ end
145
+ end
146
+
127
147
  # Whether this session has valid access tokens
128
148
  # @return [Boolean]
129
149
  def authorized?
@@ -139,30 +159,6 @@ module AuthProto
139
159
  !@tokens.nil? && @tokens.renewable?
140
160
  end
141
161
  end
142
-
143
- # Compares a supplied state token with the session token in constant time.
144
- #
145
- # @param state [String, nil] callback state token
146
- # @return [Boolean] whether it matches; `false` for `nil` or a different length
147
- #
148
- def validate_state(state)
149
- return false unless state
150
-
151
- # Use secure comparison to prevent timing attacks
152
- secure_compare(@state_token, state)
153
- end
154
-
155
- private
156
-
157
- def secure_compare(str1, str2)
158
- return false unless str1.bytesize == str2.bytesize
159
-
160
- left = str1.unpack("C*")
161
- right = str2.unpack("C*")
162
- result = 0
163
- left.zip(right) { |x, y| result |= x ^ y }
164
- result.zero?
165
- end
166
162
  end
167
163
  end
168
164
  end
@@ -25,7 +25,7 @@ module AuthProto
25
25
  #
26
26
  # @param client_id [String] OAuth client ID
27
27
  # @param scope [String] requested space-separated scopes
28
- # @param auth_server [ServerMetadata::AuthorizationServer, nil] pre-resolved authorization server
28
+ # @param auth_server [Metadata::AuthorizationServer, nil] pre-resolved authorization server
29
29
  # @param did [String, nil] pre-resolved account DID
30
30
  #
31
31
  # @return [Session] newly created session
@@ -136,7 +136,7 @@ module AuthProto
136
136
  # @return [void]
137
137
  # @raise [Storage::StorageError] if deletion or locking fails
138
138
  #
139
- def remove_session(session_id)
139
+ def delete_session(session_id)
140
140
  session = get_session(session_id)
141
141
  return unless session
142
142