authproto 0.3.0.beta1

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 (115) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +31 -0
  4. data/.tm_properties +1 -0
  5. data/.yardopts +4 -0
  6. data/CHANGELOG.md +52 -0
  7. data/MIT-LICENSE.txt +21 -0
  8. data/README.md +304 -0
  9. data/Rakefile +8 -0
  10. data/SECURITY.md +16 -0
  11. data/examples/confidential_client/.gitignore +5 -0
  12. data/examples/confidential_client/Gemfile +14 -0
  13. data/examples/confidential_client/Gemfile.lock +88 -0
  14. data/examples/confidential_client/README.md +226 -0
  15. data/examples/confidential_client/app.rb +290 -0
  16. data/examples/confidential_client/config/client-metadata.example.json +25 -0
  17. data/examples/confidential_client/config.ru +4 -0
  18. data/examples/confidential_client/public/styles.css +70 -0
  19. data/examples/confidential_client/screenshots/screenshot-1-sign-in.png +0 -0
  20. data/examples/confidential_client/screenshots/screenshot-2-success.png +0 -0
  21. data/examples/confidential_client/scripts/generate_keys.rb +15 -0
  22. data/examples/confidential_client/views/authorized.erb +46 -0
  23. data/examples/confidential_client/views/index.erb +44 -0
  24. data/examples/confidential_client/views/layout.erb +11 -0
  25. data/examples/minisky_console/.gitignore +2 -0
  26. data/examples/minisky_console/Gemfile +12 -0
  27. data/examples/minisky_console/Gemfile.lock +131 -0
  28. data/examples/minisky_console/README.md +112 -0
  29. data/examples/minisky_console/app.rb +50 -0
  30. data/examples/minisky_console/bin/console +6 -0
  31. data/examples/minisky_console/config.ru +5 -0
  32. data/examples/minisky_console/localhost.rb +53 -0
  33. data/examples/minisky_console/production.rb +32 -0
  34. data/lib/authproto/client.rb +583 -0
  35. data/lib/authproto/client_metadata.rb +478 -0
  36. data/lib/authproto/configuration.rb +139 -0
  37. data/lib/authproto/dpop/client.rb +161 -0
  38. data/lib/authproto/dpop/key_manager.rb +223 -0
  39. data/lib/authproto/dpop/nonce_manager.rb +145 -0
  40. data/lib/authproto/dpop/proof_generator.rb +124 -0
  41. data/lib/authproto/dpop/stored_nonce.rb +37 -0
  42. data/lib/authproto/encryption.rb +250 -0
  43. data/lib/authproto/errors.rb +75 -0
  44. data/lib/authproto/http_client.rb +320 -0
  45. data/lib/authproto/insecure_http_client.rb +24 -0
  46. data/lib/authproto/localhost_server.rb +274 -0
  47. data/lib/authproto/minisky.rb +273 -0
  48. data/lib/authproto/par/client.rb +240 -0
  49. data/lib/authproto/par/client_assertion.rb +69 -0
  50. data/lib/authproto/par/request.rb +243 -0
  51. data/lib/authproto/par/response.rb +43 -0
  52. data/lib/authproto/par.rb +48 -0
  53. data/lib/authproto/pkce.rb +125 -0
  54. data/lib/authproto/serialization/base.rb +258 -0
  55. data/lib/authproto/serialization/dpop_key.rb +38 -0
  56. data/lib/authproto/serialization/session.rb +88 -0
  57. data/lib/authproto/serialization/stored_nonce.rb +46 -0
  58. data/lib/authproto/serialization/token_set.rb +54 -0
  59. data/lib/authproto/server_metadata/authorization_server.rb +267 -0
  60. data/lib/authproto/server_metadata/origin_url.rb +71 -0
  61. data/lib/authproto/server_metadata/resource_server.rb +98 -0
  62. data/lib/authproto/server_metadata.rb +28 -0
  63. data/lib/authproto/state/session.rb +168 -0
  64. data/lib/authproto/state/session_manager.rb +162 -0
  65. data/lib/authproto/state/token_set.rb +101 -0
  66. data/lib/authproto/state.rb +61 -0
  67. data/lib/authproto/storage/interface.rb +121 -0
  68. data/lib/authproto/storage/key_builder.rb +76 -0
  69. data/lib/authproto/storage/memory.rb +222 -0
  70. data/lib/authproto/storage/redis.rb +156 -0
  71. data/lib/authproto/storage/sqlite.rb +281 -0
  72. data/lib/authproto/token/refresh.rb +414 -0
  73. data/lib/authproto/version.rb +8 -0
  74. data/lib/authproto.rb +18 -0
  75. data/sig/authproto/client.rbs +43 -0
  76. data/sig/authproto/client_metadata.rbs +113 -0
  77. data/sig/authproto/dependencies.rbs +19 -0
  78. data/sig/authproto/dpop/client.rbs +23 -0
  79. data/sig/authproto/dpop/key_manager.rbs +26 -0
  80. data/sig/authproto/dpop/nonce_manager.rbs +23 -0
  81. data/sig/authproto/dpop/proof_generator.rbs +40 -0
  82. data/sig/authproto/dpop/stored_nonce.rbs +10 -0
  83. data/sig/authproto/encryption.rbs +42 -0
  84. data/sig/authproto/http_client.rbs +45 -0
  85. data/sig/authproto/insecure_http_client.rbs +8 -0
  86. data/sig/authproto/localhost_server.rbs +30 -0
  87. data/sig/authproto/minisky_oauth.rbs +22 -0
  88. data/sig/authproto/par/client.rbs +25 -0
  89. data/sig/authproto/par/client_assertion.rbs +10 -0
  90. data/sig/authproto/par/request.rbs +73 -0
  91. data/sig/authproto/par/response.rbs +17 -0
  92. data/sig/authproto/par.rbs +7 -0
  93. data/sig/authproto/pkce.rbs +24 -0
  94. data/sig/authproto/serialization/base.rbs +36 -0
  95. data/sig/authproto/serialization/dpop_key.rbs +14 -0
  96. data/sig/authproto/serialization/session.rbs +19 -0
  97. data/sig/authproto/serialization/stored_nonce.rbs +14 -0
  98. data/sig/authproto/serialization/token_set.rbs +14 -0
  99. data/sig/authproto/server_metadata/authorization_server.rbs +71 -0
  100. data/sig/authproto/server_metadata/origin_url.rbs +21 -0
  101. data/sig/authproto/server_metadata/resource_server.rbs +27 -0
  102. data/sig/authproto/state/session.rbs +28 -0
  103. data/sig/authproto/state/session_manager.rbs +15 -0
  104. data/sig/authproto/state/token_set.rbs +40 -0
  105. data/sig/authproto/state.rbs +6 -0
  106. data/sig/authproto/storage/interface.rbs +20 -0
  107. data/sig/authproto/storage/key_builder.rbs +16 -0
  108. data/sig/authproto/storage/memory.rbs +22 -0
  109. data/sig/authproto/storage/redis.rbs +19 -0
  110. data/sig/authproto/storage/redis_client.rbs +11 -0
  111. data/sig/authproto/storage/sqlite.rbs +26 -0
  112. data/sig/authproto/token/refresh.rbs +47 -0
  113. data/sig/authproto/version.rbs +3 -0
  114. data/sig/authproto.rbs +54 -0
  115. metadata +200 -0
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'openssl'
5
+ require 'securerandom'
6
+
7
+ require_relative 'errors'
8
+
9
+ module AuthProto
10
+
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.
21
+ #
22
+
23
+ module PKCE
24
+
25
+ # Error raised for PKCE-related failures
26
+ class Error < AuthProto::Error; end
27
+
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
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'time'
5
+
6
+ require_relative '../encryption'
7
+ require_relative '../errors'
8
+
9
+ module AuthProto
10
+
11
+ #
12
+ # Versioned JSON serialization with encryption of sensitive OAuth fields.
13
+ #
14
+
15
+ module Serialization
16
+
17
+ # Base class for malformed serialized data and serializer validation errors.
18
+ class Error < AuthProto::Error; end
19
+
20
+ # Raised when a serialized envelope uses an invalid or unsupported version.
21
+ class VersionError < Error; end
22
+
23
+ # Raised when an envelope type does not match the selected serializer.
24
+ class TypeMismatchError < Error; end
25
+
26
+ # Raised when a serializer receives an object of the wrong type.
27
+ class ValidationError < Error; end
28
+
29
+ #
30
+ # Base serializer that all type-specific serializers inherit from
31
+ #
32
+
33
+ class Base
34
+
35
+ # Current JSON envelope schema version.
36
+ # @return [Integer]
37
+ CURRENT_VERSION = 1
38
+
39
+ # Field names encrypted recursively before JSON serialization.
40
+ # @return [Array<String>]
41
+ SENSITIVE_FIELDS = [
42
+ "access_token",
43
+ "refresh_token",
44
+ "private_key",
45
+ "d", # EC private key component
46
+ "pkce_verifier"
47
+ ].freeze
48
+
49
+ class << self
50
+
51
+ # Serializes an object into a versioned JSON envelope, encrypting sensitive fields.
52
+ #
53
+ # Concrete subclasses validate the input type and provide the data representation.
54
+ #
55
+ # @param obj [Object] object supported by the concrete serializer
56
+ # @return [String] JSON with `version`, `type`, timestamps and `data` fields
57
+ # @raise [ValidationError] if the concrete serializer rejects the object
58
+ # @raise [Encryption::EncryptionError] if a sensitive value cannot be encrypted
59
+ # @raise [NotImplementedError] if used without implementing the abstract serializer hooks
60
+ #
61
+ def serialize(obj)
62
+ new.serialize(obj)
63
+ end
64
+
65
+ # Restores an object from a versioned JSON envelope, decrypting sensitive fields.
66
+ #
67
+ # The concrete serializer determines the returned object class.
68
+ #
69
+ # @param data [String] JSON previously produced by the matching serializer
70
+ # @return [Object] reconstructed object
71
+ # @raise [Error] if JSON or the envelope format is invalid
72
+ # @raise [TypeMismatchError] if the envelope has a different type identifier
73
+ # @raise [VersionError] if the format version is invalid or unsupported
74
+ # @raise [Encryption::DecryptionError] if sensitive data cannot be authenticated/decrypted
75
+ # @raise [ArgumentError] if reconstructed fields fail validation or date/time parsing
76
+ # @raise [NotImplementedError] if used without implementing the abstract serializer hooks
77
+ #
78
+ def deserialize(data)
79
+ new.deserialize(data)
80
+ end
81
+ end
82
+
83
+ # Creates a serializer with a context-aware encryption service.
84
+ # @raise [ArgumentError] if `ATPROTO_MASTER_KEY` is not valid Base64
85
+ #
86
+ def initialize
87
+ @encryption = Encryption::Service.new
88
+ end
89
+
90
+ # Serializes an object into a versioned JSON envelope, encrypting sensitive fields.
91
+ #
92
+ # Concrete subclasses validate the input type and provide the data representation.
93
+ #
94
+ # @param obj [Object] object supported by the concrete serializer
95
+ # @return [String] JSON with `version`, `type`, timestamps and `data` fields
96
+ # @raise [ValidationError] if the concrete serializer rejects the object
97
+ # @raise [Encryption::EncryptionError] if a sensitive value cannot be encrypted
98
+ # @raise [NotImplementedError] if used without implementing the abstract serializer hooks
99
+ #
100
+ def serialize(obj)
101
+ validate_object(obj)
102
+
103
+ # First serialize the object
104
+ data = {
105
+ version: CURRENT_VERSION,
106
+ type: type_identifier,
107
+ created_at: Time.now.utc.iso8601,
108
+ updated_at: Time.now.utc.iso8601,
109
+ data: serialize_data(obj)
110
+ }
111
+
112
+ # Then encrypt sensitive fields
113
+ encrypt_sensitive_fields(data)
114
+
115
+ # Finally convert to JSON
116
+ JSON.generate(data)
117
+ end
118
+
119
+ # Restores an object from a versioned JSON envelope, decrypting sensitive fields.
120
+ #
121
+ # The concrete serializer determines the returned object class.
122
+ #
123
+ # @param data [String] JSON previously produced by the matching serializer
124
+ # @return [Object] reconstructed object
125
+ # @raise [Error] if JSON or the envelope format is invalid
126
+ # @raise [TypeMismatchError] if the envelope has a different type identifier
127
+ # @raise [VersionError] if the format version is invalid or unsupported
128
+ # @raise [Encryption::DecryptionError] if sensitive data cannot be authenticated/decrypted
129
+ # @raise [ArgumentError] if reconstructed fields fail validation or date/time parsing
130
+ # @raise [NotImplementedError] if used without implementing the abstract serializer hooks
131
+ #
132
+ def deserialize(data)
133
+ parsed = parse_json(data)
134
+ validate_serialized_data(parsed)
135
+
136
+ # Decrypt sensitive fields
137
+ decrypt_sensitive_fields(parsed)
138
+
139
+ # Deserialize according to version
140
+ deserialize_version(parsed)
141
+ end
142
+
143
+ private
144
+
145
+ def encrypt_sensitive_fields(data, path: [])
146
+ return unless data.is_a?(Hash)
147
+
148
+ data.each do |key, value|
149
+ current_path = path + [key]
150
+
151
+ if sensitive_field?(key)
152
+ data[key] = @encryption.encrypt(
153
+ value.to_s,
154
+ context: current_path.join(".")
155
+ )
156
+ elsif value.is_a?(Hash)
157
+ encrypt_sensitive_fields(value, path: current_path)
158
+ elsif value.is_a?(Array)
159
+ value.each_with_index do |v, i|
160
+ encrypt_sensitive_fields(v, path: current_path + [i.to_s]) if v.is_a?(Hash)
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ def decrypt_sensitive_fields(data, path: [])
167
+ return unless data.is_a?(Hash)
168
+
169
+ data.each do |key, value|
170
+ current_path = path + [key]
171
+ if sensitive_field?(key)
172
+ data[key] = @encryption.decrypt(
173
+ value.transform_keys(&:to_sym),
174
+ context: current_path.join(".")
175
+ )
176
+ elsif value.is_a?(Hash)
177
+ decrypt_sensitive_fields(value, path: current_path)
178
+ elsif value.is_a?(Array)
179
+ value.each_with_index do |v, i|
180
+ decrypt_sensitive_fields(v, path: current_path + [i.to_s]) if v.is_a?(Hash)
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ def sensitive_field?(field)
187
+ SENSITIVE_FIELDS.any? do |sensitive_field|
188
+ field.to_s == sensitive_field
189
+ end
190
+ end
191
+
192
+ # Type identifier for serialized data
193
+ # @return [String]
194
+ def type_identifier
195
+ raise NotImplementedError
196
+ end
197
+
198
+ # Serialize object to hash
199
+ #
200
+ # @param obj [Object] Object to serialize
201
+ # @return [Hash] Serialized data
202
+ #
203
+ def serialize_data(obj)
204
+ raise NotImplementedError
205
+ end
206
+
207
+ # Deserialize object from hash
208
+ #
209
+ # @param data [Hash] Serialized data
210
+ # @return [Object] Deserialized object
211
+ #
212
+ def deserialize_data(data)
213
+ raise NotImplementedError
214
+ end
215
+
216
+ # Validate object before serialization
217
+ #
218
+ # @param obj [Object] Object to validate
219
+ # @raise [ValidationError] if object is invalid
220
+ #
221
+ def validate_object(obj)
222
+ raise NotImplementedError
223
+ end
224
+
225
+ def parse_json(data)
226
+ JSON.parse(data)
227
+ rescue JSON::ParserError => e
228
+ raise Error, "Invalid JSON data: #{e.message}"
229
+ end
230
+
231
+ def validate_serialized_data(data)
232
+ raise Error, "Invalid serialized data format" unless data.is_a?(Hash)
233
+
234
+ unless data["type"] == type_identifier
235
+ raise TypeMismatchError,
236
+ "Expected type #{type_identifier}, got #{data["type"]}"
237
+ end
238
+
239
+ raise VersionError, "Invalid version format" unless data["version"].is_a?(Integer)
240
+
241
+ return unless data["version"] > CURRENT_VERSION
242
+
243
+ raise VersionError,
244
+ "Version #{data["version"]} not supported"
245
+ end
246
+
247
+ def deserialize_version(data)
248
+ case data["version"]
249
+ when 1
250
+ deserialize_data(data["data"])
251
+ else
252
+ raise VersionError,
253
+ "Unknown version: #{data["version"]}"
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../dpop/key_manager'
4
+ require_relative 'base'
5
+
6
+ module AuthProto
7
+ module Serialization
8
+
9
+ #
10
+ # Handles serialization of DPoP keys
11
+ #
12
+
13
+ class DPoPKey < Base
14
+
15
+ # Returns the envelope type used by this serializer.
16
+ # @return [String] `DPoPKey`
17
+ def type_identifier
18
+ "DPoPKey"
19
+ end
20
+
21
+ private
22
+
23
+ def validate_object(obj)
24
+ return if obj.is_a?(AuthProto::DPoP::KeyManager)
25
+
26
+ raise ValidationError, "Expected KeyManager object, got #{obj.class}"
27
+ end
28
+
29
+ def serialize_data(obj)
30
+ obj.to_jwk(include_private: true).to_h
31
+ end
32
+
33
+ def deserialize_data(data)
34
+ AuthProto::DPoP::KeyManager.from_jwk(data)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../server_metadata/authorization_server'
4
+ require_relative '../state/session'
5
+ require_relative 'base'
6
+ require_relative 'token_set'
7
+
8
+ module AuthProto
9
+ module Serialization
10
+
11
+ #
12
+ # Handles serialization of Session objects
13
+ #
14
+
15
+ class Session < Base
16
+
17
+ # Returns the envelope type used by this serializer.
18
+ # @return [String] `Session`
19
+ def type_identifier
20
+ "Session"
21
+ end
22
+
23
+ private
24
+
25
+ def validate_object(obj)
26
+ return if obj.is_a?(AuthProto::State::Session)
27
+
28
+ raise ValidationError, "Expected Session object, got #{obj.class}"
29
+ end
30
+
31
+ def serialize_data(obj)
32
+ {
33
+ session_id: obj.session_id,
34
+ state_token: obj.state_token,
35
+ client_id: obj.client_id,
36
+ scope: obj.scope,
37
+ pkce_verifier: obj.pkce_verifier,
38
+ pkce_challenge: obj.pkce_challenge,
39
+ did: obj.did,
40
+ tokens: serialize_token_set(obj.tokens),
41
+ auth_server: serialize_auth_server(obj.auth_server)
42
+ }
43
+ end
44
+
45
+ def deserialize_data(data)
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"])
51
+ ).tap do |session|
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"])
57
+
58
+ # Set tokens if present
59
+ session.tokens = deserialize_token_set(data["tokens"]) if data["tokens"]
60
+ end
61
+ end
62
+
63
+ def serialize_token_set(tokens)
64
+ return nil unless tokens
65
+
66
+ TokenSet.new.serialize(tokens)
67
+ end
68
+
69
+ def deserialize_token_set(data)
70
+ return nil unless data
71
+
72
+ TokenSet.new.deserialize(data)
73
+ end
74
+
75
+ def serialize_auth_server(server)
76
+ return nil unless server
77
+
78
+ server.to_h
79
+ end
80
+
81
+ def deserialize_auth_server(data)
82
+ return nil unless data
83
+
84
+ AuthProto::ServerMetadata::AuthorizationServer.new(data)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../dpop/stored_nonce'
4
+ require_relative 'base'
5
+
6
+ module AuthProto
7
+ module Serialization
8
+
9
+ #
10
+ # Handles serialization of StoredNonce objects
11
+ #
12
+
13
+ class StoredNonce < Base
14
+
15
+ # Returns the envelope type used by this serializer.
16
+ # @return [String] `StoredNonce`
17
+ def type_identifier
18
+ "StoredNonce"
19
+ end
20
+
21
+ private
22
+
23
+ def validate_object(obj)
24
+ return if obj.is_a?(AuthProto::DPoP::StoredNonce)
25
+
26
+ raise ValidationError, "Expected StoredNonce object, got #{obj.class}"
27
+ end
28
+
29
+ def serialize_data(obj)
30
+ {
31
+ value: obj.value,
32
+ server_url: obj.server_url,
33
+ timestamp: obj.timestamp
34
+ }
35
+ end
36
+
37
+ def deserialize_data(data)
38
+ AuthProto::DPoP::StoredNonce.new(
39
+ data["value"],
40
+ data["server_url"],
41
+ timestamp: Time.at(data["timestamp"])
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ require_relative '../state/token_set'
6
+ require_relative 'base'
7
+
8
+ module AuthProto
9
+ module Serialization
10
+
11
+ #
12
+ # Handles serialization of TokenSet objects
13
+ #
14
+
15
+ class TokenSet < Base
16
+
17
+ # Returns the envelope type used by this serializer.
18
+ # @return [String] `TokenSet`
19
+ def type_identifier
20
+ "TokenSet"
21
+ end
22
+
23
+ private
24
+
25
+ def validate_object(obj)
26
+ return if obj.is_a?(AuthProto::State::TokenSet)
27
+
28
+ raise ValidationError, "Expected TokenSet object, got #{obj.class}"
29
+ end
30
+
31
+ def serialize_data(obj)
32
+ {
33
+ access_token: obj.access_token,
34
+ refresh_token: obj.refresh_token,
35
+ token_type: obj.token_type,
36
+ expires_at: obj.expires_at.utc.iso8601,
37
+ scope: obj.scope,
38
+ sub: obj.sub
39
+ }
40
+ end
41
+
42
+ def deserialize_data(data)
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"]
50
+ )
51
+ end
52
+ end
53
+ end
54
+ end