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.
- checksums.yaml +7 -0
- data/.rspec +1 -0
- data/.rubocop.yml +31 -0
- data/.tm_properties +1 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +52 -0
- data/MIT-LICENSE.txt +21 -0
- data/README.md +304 -0
- data/Rakefile +8 -0
- data/SECURITY.md +16 -0
- data/examples/confidential_client/.gitignore +5 -0
- data/examples/confidential_client/Gemfile +14 -0
- data/examples/confidential_client/Gemfile.lock +88 -0
- data/examples/confidential_client/README.md +226 -0
- data/examples/confidential_client/app.rb +290 -0
- data/examples/confidential_client/config/client-metadata.example.json +25 -0
- data/examples/confidential_client/config.ru +4 -0
- data/examples/confidential_client/public/styles.css +70 -0
- data/examples/confidential_client/screenshots/screenshot-1-sign-in.png +0 -0
- data/examples/confidential_client/screenshots/screenshot-2-success.png +0 -0
- data/examples/confidential_client/scripts/generate_keys.rb +15 -0
- data/examples/confidential_client/views/authorized.erb +46 -0
- data/examples/confidential_client/views/index.erb +44 -0
- data/examples/confidential_client/views/layout.erb +11 -0
- data/examples/minisky_console/.gitignore +2 -0
- data/examples/minisky_console/Gemfile +12 -0
- data/examples/minisky_console/Gemfile.lock +131 -0
- data/examples/minisky_console/README.md +112 -0
- data/examples/minisky_console/app.rb +50 -0
- data/examples/minisky_console/bin/console +6 -0
- data/examples/minisky_console/config.ru +5 -0
- data/examples/minisky_console/localhost.rb +53 -0
- data/examples/minisky_console/production.rb +32 -0
- data/lib/authproto/client.rb +583 -0
- data/lib/authproto/client_metadata.rb +478 -0
- data/lib/authproto/configuration.rb +139 -0
- data/lib/authproto/dpop/client.rb +161 -0
- data/lib/authproto/dpop/key_manager.rb +223 -0
- data/lib/authproto/dpop/nonce_manager.rb +145 -0
- data/lib/authproto/dpop/proof_generator.rb +124 -0
- data/lib/authproto/dpop/stored_nonce.rb +37 -0
- data/lib/authproto/encryption.rb +250 -0
- data/lib/authproto/errors.rb +75 -0
- data/lib/authproto/http_client.rb +320 -0
- data/lib/authproto/insecure_http_client.rb +24 -0
- data/lib/authproto/localhost_server.rb +274 -0
- data/lib/authproto/minisky.rb +273 -0
- data/lib/authproto/par/client.rb +240 -0
- data/lib/authproto/par/client_assertion.rb +69 -0
- data/lib/authproto/par/request.rb +243 -0
- data/lib/authproto/par/response.rb +43 -0
- data/lib/authproto/par.rb +48 -0
- data/lib/authproto/pkce.rb +125 -0
- data/lib/authproto/serialization/base.rb +258 -0
- data/lib/authproto/serialization/dpop_key.rb +38 -0
- data/lib/authproto/serialization/session.rb +88 -0
- data/lib/authproto/serialization/stored_nonce.rb +46 -0
- data/lib/authproto/serialization/token_set.rb +54 -0
- data/lib/authproto/server_metadata/authorization_server.rb +267 -0
- data/lib/authproto/server_metadata/origin_url.rb +71 -0
- data/lib/authproto/server_metadata/resource_server.rb +98 -0
- data/lib/authproto/server_metadata.rb +28 -0
- data/lib/authproto/state/session.rb +168 -0
- data/lib/authproto/state/session_manager.rb +162 -0
- data/lib/authproto/state/token_set.rb +101 -0
- data/lib/authproto/state.rb +61 -0
- data/lib/authproto/storage/interface.rb +121 -0
- data/lib/authproto/storage/key_builder.rb +76 -0
- data/lib/authproto/storage/memory.rb +222 -0
- data/lib/authproto/storage/redis.rb +156 -0
- data/lib/authproto/storage/sqlite.rb +281 -0
- data/lib/authproto/token/refresh.rb +414 -0
- data/lib/authproto/version.rb +8 -0
- data/lib/authproto.rb +18 -0
- data/sig/authproto/client.rbs +43 -0
- data/sig/authproto/client_metadata.rbs +113 -0
- data/sig/authproto/dependencies.rbs +19 -0
- data/sig/authproto/dpop/client.rbs +23 -0
- data/sig/authproto/dpop/key_manager.rbs +26 -0
- data/sig/authproto/dpop/nonce_manager.rbs +23 -0
- data/sig/authproto/dpop/proof_generator.rbs +40 -0
- data/sig/authproto/dpop/stored_nonce.rbs +10 -0
- data/sig/authproto/encryption.rbs +42 -0
- data/sig/authproto/http_client.rbs +45 -0
- data/sig/authproto/insecure_http_client.rbs +8 -0
- data/sig/authproto/localhost_server.rbs +30 -0
- data/sig/authproto/minisky_oauth.rbs +22 -0
- data/sig/authproto/par/client.rbs +25 -0
- data/sig/authproto/par/client_assertion.rbs +10 -0
- data/sig/authproto/par/request.rbs +73 -0
- data/sig/authproto/par/response.rbs +17 -0
- data/sig/authproto/par.rbs +7 -0
- data/sig/authproto/pkce.rbs +24 -0
- data/sig/authproto/serialization/base.rbs +36 -0
- data/sig/authproto/serialization/dpop_key.rbs +14 -0
- data/sig/authproto/serialization/session.rbs +19 -0
- data/sig/authproto/serialization/stored_nonce.rbs +14 -0
- data/sig/authproto/serialization/token_set.rbs +14 -0
- data/sig/authproto/server_metadata/authorization_server.rbs +71 -0
- data/sig/authproto/server_metadata/origin_url.rbs +21 -0
- data/sig/authproto/server_metadata/resource_server.rbs +27 -0
- data/sig/authproto/state/session.rbs +28 -0
- data/sig/authproto/state/session_manager.rbs +15 -0
- data/sig/authproto/state/token_set.rbs +40 -0
- data/sig/authproto/state.rbs +6 -0
- data/sig/authproto/storage/interface.rbs +20 -0
- data/sig/authproto/storage/key_builder.rbs +16 -0
- data/sig/authproto/storage/memory.rbs +22 -0
- data/sig/authproto/storage/redis.rbs +19 -0
- data/sig/authproto/storage/redis_client.rbs +11 -0
- data/sig/authproto/storage/sqlite.rbs +26 -0
- data/sig/authproto/token/refresh.rbs +47 -0
- data/sig/authproto/version.rbs +3 -0
- data/sig/authproto.rbs +54 -0
- 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
|