atproto_auth 0.0.1 → 0.1.0

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +17 -2
  3. data/CHANGELOG.md +23 -2
  4. data/PROJECT_STRUCTURE.txt +10129 -0
  5. data/README.md +88 -2
  6. data/examples/confidential_client/.gitignore +2 -0
  7. data/examples/confidential_client/Gemfile.lock +6 -0
  8. data/examples/confidential_client/README.md +86 -9
  9. data/examples/confidential_client/app.rb +83 -12
  10. data/examples/confidential_client/{public/client-metadata.json → config/client-metadata.example.json} +5 -4
  11. data/examples/confidential_client/screenshots/screenshot-1-sign-in.png +0 -0
  12. data/examples/confidential_client/screenshots/screenshot-2-success.png +0 -0
  13. data/examples/confidential_client/scripts/generate_keys.rb +0 -0
  14. data/examples/confidential_client/views/authorized.erb +1 -1
  15. data/lib/atproto_auth/client.rb +98 -38
  16. data/lib/atproto_auth/client_metadata.rb +2 -2
  17. data/lib/atproto_auth/configuration.rb +35 -1
  18. data/lib/atproto_auth/dpop/key_manager.rb +1 -1
  19. data/lib/atproto_auth/dpop/nonce_manager.rb +30 -47
  20. data/lib/atproto_auth/encryption.rb +156 -0
  21. data/lib/atproto_auth/http_client.rb +2 -2
  22. data/lib/atproto_auth/identity/document.rb +1 -1
  23. data/lib/atproto_auth/identity/resolver.rb +1 -1
  24. data/lib/atproto_auth/serialization/base.rb +189 -0
  25. data/lib/atproto_auth/serialization/dpop_key.rb +29 -0
  26. data/lib/atproto_auth/serialization/session.rb +77 -0
  27. data/lib/atproto_auth/serialization/stored_nonce.rb +37 -0
  28. data/lib/atproto_auth/serialization/token_set.rb +43 -0
  29. data/lib/atproto_auth/server_metadata/authorization_server.rb +20 -1
  30. data/lib/atproto_auth/state/session_manager.rb +67 -20
  31. data/lib/atproto_auth/storage/interface.rb +112 -0
  32. data/lib/atproto_auth/storage/key_builder.rb +39 -0
  33. data/lib/atproto_auth/storage/memory.rb +191 -0
  34. data/lib/atproto_auth/storage/redis.rb +119 -0
  35. data/lib/atproto_auth/token/refresh.rb +249 -0
  36. data/lib/atproto_auth/version.rb +1 -1
  37. data/lib/atproto_auth.rb +29 -1
  38. metadata +32 -4
  39. data/examples/confidential_client/config/client-metadata.json +0 -25
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module AtprotoAuth
6
+ module Storage
7
+ # Thread-safe in-memory storage implementation
8
+ class Memory < Interface
9
+ def initialize
10
+ super
11
+ @data = {}
12
+ @locks = {}
13
+ @expirations = {}
14
+ @monitor = Monitor.new
15
+ @cleanup_interval = 60 # 1 minute
16
+ start_cleanup_thread
17
+ end
18
+
19
+ def set(key, value, ttl: nil)
20
+ validate_key!(key)
21
+ validate_ttl!(ttl)
22
+
23
+ @monitor.synchronize do
24
+ @data[key] = value
25
+ set_expiration(key, ttl) if ttl
26
+ true
27
+ end
28
+ end
29
+
30
+ def get(key)
31
+ validate_key!(key)
32
+
33
+ @monitor.synchronize do
34
+ return nil if expired?(key)
35
+
36
+ @data[key]
37
+ end
38
+ end
39
+
40
+ def delete(key)
41
+ validate_key!(key)
42
+
43
+ @monitor.synchronize do
44
+ @data.delete(key)
45
+ @expirations.delete(key)
46
+ true
47
+ end
48
+ end
49
+
50
+ def exists?(key)
51
+ validate_key!(key)
52
+
53
+ @monitor.synchronize do
54
+ return false unless @data.key?(key)
55
+ return false if expired?(key)
56
+
57
+ true
58
+ end
59
+ end
60
+
61
+ def multi_get(keys)
62
+ keys.each { |key| validate_key!(key) }
63
+
64
+ @monitor.synchronize do
65
+ result = {}
66
+ keys.each do |key|
67
+ result[key] = @data[key] if @data.key?(key) && !expired?(key)
68
+ end
69
+ result
70
+ end
71
+ end
72
+
73
+ def multi_set(hash, ttl: nil)
74
+ hash.each_key { |key| validate_key!(key) }
75
+ validate_ttl!(ttl)
76
+
77
+ @monitor.synchronize do
78
+ hash.each do |key, value|
79
+ @data[key] = value
80
+ set_expiration(key, ttl) if ttl
81
+ end
82
+ true
83
+ end
84
+ end
85
+
86
+ def acquire_lock(key, ttl:)
87
+ validate_key!(key)
88
+ validate_ttl!(ttl)
89
+ lock_key = "lock:#{key}"
90
+
91
+ @monitor.synchronize do
92
+ return false if @locks[lock_key] && !lock_expired?(lock_key)
93
+
94
+ @locks[lock_key] = Time.now.to_i + ttl
95
+ true
96
+ end
97
+ end
98
+
99
+ def release_lock(key)
100
+ validate_key!(key)
101
+ lock_key = "lock:#{key}"
102
+
103
+ @monitor.synchronize do
104
+ @locks.delete(lock_key)
105
+ true
106
+ end
107
+ end
108
+
109
+ def with_lock(key, ttl: 30)
110
+ raise ArgumentError, "Block required" unless block_given?
111
+
112
+ acquired = acquire_lock(key, ttl: ttl)
113
+ raise LockError, "Failed to acquire lock" unless acquired
114
+
115
+ begin
116
+ yield
117
+ ensure
118
+ release_lock(key)
119
+ end
120
+ end
121
+
122
+ def clear
123
+ @monitor.synchronize do
124
+ @data.clear
125
+ @expirations.clear
126
+ @locks.clear
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ def expired?(key)
133
+ return false unless @expirations.key?(key)
134
+
135
+ expiry = @expirations[key]
136
+ if Time.now.to_i >= expiry
137
+ @data.delete(key)
138
+ @expirations.delete(key)
139
+ true
140
+ else
141
+ false
142
+ end
143
+ end
144
+
145
+ def lock_expired?(lock_key)
146
+ expiry = @locks[lock_key]
147
+ return false unless expiry
148
+
149
+ if Time.now.to_i >= expiry
150
+ @locks.delete(lock_key)
151
+ true
152
+ else
153
+ false
154
+ end
155
+ end
156
+
157
+ def set_expiration(key, ttl)
158
+ @expirations[key] = Time.now.to_i + ttl
159
+ end
160
+
161
+ def cleanup_expired
162
+ @monitor.synchronize do
163
+ now = Time.now.to_i
164
+
165
+ # Cleanup expired data
166
+ @expirations.each do |key, expiry|
167
+ if now >= expiry
168
+ @data.delete(key)
169
+ @expirations.delete(key)
170
+ end
171
+ end
172
+
173
+ # Cleanup expired locks
174
+ @locks.delete_if { |_, expiry| now >= expiry }
175
+ end
176
+ end
177
+
178
+ def start_cleanup_thread
179
+ Thread.new do
180
+ loop do
181
+ sleep @cleanup_interval
182
+ cleanup_expired
183
+ rescue StandardError => e
184
+ # Log error but keep thread running
185
+ AtprotoAuth.configuration.logger.error "Storage cleanup error: #{e.message}"
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redis"
4
+
5
+ module AtprotoAuth
6
+ module Storage
7
+ # Redis storage implementation
8
+ class Redis < Interface
9
+ # Error raised for Redis-specific issues
10
+ class RedisError < StorageError; end
11
+
12
+ def initialize(redis_client: nil)
13
+ super()
14
+ @redis_client = redis_client || ::Redis.new
15
+ end
16
+
17
+ def set(key, value, ttl: nil)
18
+ validate_key!(key)
19
+ validate_ttl!(ttl) if ttl
20
+
21
+ @redis_client.set(key, value, ex: ttl)
22
+ true
23
+ rescue ::Redis::BaseError => e
24
+ raise RedisError, "Failed to set value: #{e.message}"
25
+ end
26
+
27
+ def get(key)
28
+ validate_key!(key)
29
+
30
+ value = @redis_client.get(key)
31
+ value.nil? || value == "" ? nil : value
32
+ rescue ::Redis::BaseError => e
33
+ raise RedisError, "Failed to get value: #{e.message}"
34
+ end
35
+
36
+ def delete(key)
37
+ validate_key!(key)
38
+
39
+ @redis_client.del(key).positive?
40
+ rescue ::Redis::BaseError => e
41
+ raise RedisError, "Failed to delete value: #{e.message}"
42
+ end
43
+
44
+ def exists?(key)
45
+ validate_key!(key)
46
+
47
+ @redis_client.exists?(key)
48
+ rescue ::Redis::BaseError => e
49
+ raise RedisError, "Failed to check existence: #{e.message}"
50
+ end
51
+
52
+ def multi_get(keys)
53
+ keys.each { |key| validate_key!(key) }
54
+
55
+ values = @redis_client.mget(keys)
56
+ result = {}
57
+
58
+ # Only include non-nil values in result hash
59
+ keys.zip(values).each do |key, value|
60
+ next if value.nil? || value == ""
61
+
62
+ result[key] = value
63
+ end
64
+
65
+ result
66
+ rescue ::Redis::BaseError => e
67
+ raise RedisError, "Failed to get multiple values: #{e.message}"
68
+ end
69
+
70
+ def multi_set(hash, ttl: nil)
71
+ hash.each_key { |key| validate_key!(key) }
72
+ validate_ttl!(ttl) if ttl
73
+
74
+ @redis_client.multi do |tx|
75
+ hash.each do |key, value|
76
+ tx.set(key, value, ex: ttl)
77
+ end
78
+ end
79
+ true
80
+ rescue ::Redis::BaseError => e
81
+ raise RedisError, "Failed to set multiple values: #{e.message}"
82
+ end
83
+
84
+ def acquire_lock(key, ttl:)
85
+ validate_key!(key)
86
+ validate_ttl!(ttl)
87
+
88
+ lock_key = "atproto:locks:#{key}"
89
+ @redis_client.set(lock_key, Time.now.to_i, nx: true, ex: ttl)
90
+ rescue ::Redis::BaseError => e
91
+ raise RedisError, "Failed to acquire lock: #{e.message}"
92
+ end
93
+
94
+ def release_lock(key)
95
+ validate_key!(key)
96
+
97
+ lock_key = "atproto:locks:#{key}"
98
+ @redis_client.del(lock_key).positive?
99
+ rescue ::Redis::BaseError => e
100
+ raise RedisError, "Failed to release lock: #{e.message}"
101
+ end
102
+
103
+ def with_lock(key, ttl: 30)
104
+ raise ArgumentError, "Block required" unless block_given?
105
+
106
+ acquired = acquire_lock(key, ttl: ttl)
107
+ raise LockError, "Failed to acquire lock" unless acquired
108
+
109
+ begin
110
+ yield
111
+ ensure
112
+ release_lock(key)
113
+ end
114
+ rescue ::Redis::BaseError => e
115
+ raise RedisError, "Lock operation failed: #{e.message}"
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtprotoAuth
4
+ module Token
5
+ # Base error class for token-related errors
6
+ class Error < AtprotoAuth::Error
7
+ attr_reader :token_type, :error_code, :retry_possible
8
+
9
+ def initialize(message, token_type:, error_code:, retry_possible: false)
10
+ @token_type = token_type
11
+ @error_code = error_code
12
+ @retry_possible = retry_possible
13
+ super(message)
14
+ end
15
+ end
16
+
17
+ # Specific token error types
18
+ class ExpiredTokenError < Error
19
+ def initialize(token_type:)
20
+ super(
21
+ "Token has expired",
22
+ token_type: token_type,
23
+ error_code: "token_expired",
24
+ retry_possible: true
25
+ )
26
+ end
27
+ end
28
+
29
+ # Raised when a token is structurally valid but has been invalidated or revoked
30
+ class InvalidTokenError < Error
31
+ def initialize(token_type:)
32
+ super(
33
+ "Token is invalid",
34
+ token_type: token_type,
35
+ error_code: "token_invalid",
36
+ retry_possible: false
37
+ )
38
+ end
39
+ end
40
+
41
+ # Raised during token refresh operations, includes retry information and server responses
42
+ class RefreshError < Error
43
+ def initialize(message, retry_possible: true)
44
+ super(
45
+ message,
46
+ token_type: "refresh",
47
+ error_code: "refresh_failed",
48
+ retry_possible: retry_possible
49
+ )
50
+ end
51
+ end
52
+
53
+ # Handles token refresh operations with retry logic
54
+ class Refresh
55
+ include MonitorMixin
56
+
57
+ # Maximum number of refresh attempts
58
+ MAX_RETRIES = 3
59
+ # Base delay between retries in seconds
60
+ BASE_DELAY = 1
61
+ # Maximum delay between retries in seconds
62
+ MAX_DELAY = 8
63
+
64
+ attr_reader :session, :dpop_client, :auth_server, :client_metadata
65
+
66
+ def initialize(session:, dpop_client:, auth_server:, client_metadata:)
67
+ super() # Initialize MonitorMixin
68
+ @session = session
69
+ @dpop_client = dpop_client
70
+ @auth_server = auth_server
71
+ @attempt_count = 0
72
+ @client_metadata = client_metadata
73
+ end
74
+
75
+ # Performs token refresh with retry logic
76
+ # @return [TokenSet] New token set
77
+ # @raise [RefreshError] if refresh fails
78
+ def perform!
79
+ synchronize do
80
+ raise RefreshError.new("No refresh token available", retry_possible: false) unless session.renewable?
81
+ raise RefreshError.new("No access token to refresh", retry_possible: false) if session.tokens.nil?
82
+
83
+ with_retries do
84
+ request_token_refresh
85
+ end
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def with_retries
92
+ @attempt_count = 0
93
+ last_error = nil
94
+
95
+ while @attempt_count < MAX_RETRIES
96
+ begin
97
+ return yield
98
+ rescue StandardError => e
99
+ last_error = e
100
+ @attempt_count += 1
101
+
102
+ # Don't retry if error indicates retry not possible
103
+ raise e if e.respond_to?(:retry_possible) && !e.retry_possible
104
+
105
+ sleep calculate_delay if @attempt_count < MAX_RETRIES
106
+ end
107
+ end
108
+
109
+ # Reached max retries
110
+ raise RefreshError.new(
111
+ "Token refresh failed after #{MAX_RETRIES} attempts: #{last_error.message}",
112
+ retry_possible: false
113
+ )
114
+ end
115
+
116
+ def calculate_delay
117
+ # Exponential backoff with jitter
118
+ delay = [BASE_DELAY * (2**(@attempt_count - 1)), MAX_DELAY].min
119
+ delay + (rand * 0.5 * delay) # Add up to 50% jitter
120
+ end
121
+
122
+ def request_token_refresh
123
+ # Generate DPoP proof for token request
124
+ proof = dpop_client.generate_proof(
125
+ http_method: "POST",
126
+ http_uri: auth_server.token_endpoint
127
+ )
128
+
129
+ # Build refresh request
130
+ body = {
131
+ grant_type: "refresh_token",
132
+ refresh_token: session.tokens.refresh_token,
133
+ scope: session.scope
134
+ }
135
+
136
+ # Add client authentication if available
137
+ add_client_authentication!(body) if client_metadata.confidential?
138
+
139
+ # Make request
140
+ response = AtprotoAuth.configuration.http_client.post(
141
+ auth_server.token_endpoint,
142
+ body: body,
143
+ headers: {
144
+ "Content-Type" => "application/x-www-form-urlencoded",
145
+ "DPoP" => proof
146
+ }
147
+ )
148
+
149
+ handle_refresh_response(response)
150
+ end
151
+
152
+ def add_client_authentication!(body)
153
+ return unless session.client_metadata.jwks && !session.client_metadata.jwks["keys"].empty?
154
+
155
+ signing_key = JOSE::JWK.from_map(session.client_metadata.jwks["keys"].first)
156
+ client_assertion = PAR::ClientAssertion.new(
157
+ client_id: session.client_metadata.client_id,
158
+ signing_key: signing_key
159
+ )
160
+
161
+ body.merge!(
162
+ client_assertion_type: PAR::CLIENT_ASSERTION_TYPE,
163
+ client_assertion: client_assertion.generate_jwt(
164
+ audience: auth_server.issuer
165
+ )
166
+ )
167
+ end
168
+
169
+ def handle_refresh_response(response)
170
+ case response[:status]
171
+ when 200
172
+ process_successful_response(response)
173
+ when 400
174
+ handle_400_response(response)
175
+ when 401
176
+ raise RefreshError.new("Refresh token is invalid", retry_possible: false)
177
+ when 429
178
+ handle_rate_limit_response(response)
179
+ else
180
+ raise RefreshError, "Unexpected response: #{response[:status]}"
181
+ end
182
+ end
183
+
184
+ def process_successful_response(response)
185
+ data = JSON.parse(response[:body])
186
+ validate_refresh_response!(data)
187
+
188
+ AtprotoAuth::State::TokenSet.new(
189
+ access_token: data["access_token"],
190
+ token_type: data["token_type"],
191
+ expires_in: data["expires_in"],
192
+ refresh_token: data["refresh_token"],
193
+ scope: data["scope"],
194
+ sub: data["sub"]
195
+ )
196
+ rescue JSON::ParserError => e
197
+ raise RefreshError, "Invalid response format: #{e.message}"
198
+ end
199
+
200
+ def handle_400_response(response)
201
+ error_data = JSON.parse(response[:body])
202
+ error_description = error_data["error_description"] || error_data["error"]
203
+
204
+ # Handle DPoP nonce requirement
205
+ if error_data["error"] == "use_dpop_nonce"
206
+ dpop_client.process_response(response[:headers], auth_server.issuer)
207
+ raise RefreshError, "Retry with DPoP nonce"
208
+ end
209
+
210
+ raise RefreshError.new(
211
+ "Refresh request failed: #{error_description}",
212
+ retry_possible: false
213
+ )
214
+ rescue JSON::ParserError
215
+ raise RefreshError, "Invalid error response format"
216
+ end
217
+
218
+ def handle_rate_limit_response(response)
219
+ # Extract retry-after if available
220
+ retry_after = response[:headers]["Retry-After"]&.to_i || calculate_delay
221
+ raise RefreshError, "Rate limited - retry after #{retry_after} seconds"
222
+ end
223
+
224
+ def validate_refresh_response!(data)
225
+ # Required fields
226
+ %w[access_token token_type expires_in scope sub].each do |field|
227
+ raise RefreshError.new("Missing #{field} in response", retry_possible: false) unless data[field]
228
+ end
229
+
230
+ # Token type must be DPoP
231
+ unless data["token_type"] == "DPoP"
232
+ raise RefreshError.new("Invalid token_type: #{data["token_type"]}", retry_possible: false)
233
+ end
234
+
235
+ # Scope must include original scopes
236
+ original_scopes = session.scope.split
237
+ response_scopes = data["scope"].split
238
+ unless (original_scopes - response_scopes).empty?
239
+ raise RefreshError.new("Invalid scope in response", retry_possible: false)
240
+ end
241
+
242
+ # Subject must match
243
+ return if data["sub"] == session.tokens.sub
244
+
245
+ raise RefreshError.new("Subject mismatch in response", retry_possible: false)
246
+ end
247
+ end
248
+ end
249
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtprotoAuth
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
5
5
  end
data/lib/atproto_auth.rb CHANGED
@@ -5,12 +5,18 @@ require "jwt"
5
5
 
6
6
  require "atproto_auth/version"
7
7
 
8
- require "atproto_auth/configuration"
9
8
  require "atproto_auth/errors"
9
+ require "atproto_auth/configuration"
10
+ require "atproto_auth/encryption"
10
11
  require "atproto_auth/client_metadata"
11
12
  require "atproto_auth/http_client"
12
13
  require "atproto_auth/pkce"
13
14
 
15
+ require "atproto_auth/storage/interface"
16
+ require "atproto_auth/storage/key_builder"
17
+ require "atproto_auth/storage/memory"
18
+ require "atproto_auth/storage/redis"
19
+
14
20
  require "atproto_auth/server_metadata"
15
21
  require "atproto_auth/server_metadata/origin_url"
16
22
  require "atproto_auth/server_metadata/authorization_server"
@@ -36,6 +42,14 @@ require "atproto_auth/par/request"
36
42
  require "atproto_auth/par/response"
37
43
  require "atproto_auth/par/client"
38
44
 
45
+ require "atproto_auth/serialization/base"
46
+ require "atproto_auth/serialization/dpop_key"
47
+ require "atproto_auth/serialization/session"
48
+ require "atproto_auth/serialization/stored_nonce"
49
+ require "atproto_auth/serialization/token_set"
50
+
51
+ require "atproto_auth/token/refresh"
52
+
39
53
  require "atproto_auth/client"
40
54
 
41
55
  # AtprotoAuth is a Ruby library implementing the AT Protocol OAuth specification.
@@ -51,6 +65,20 @@ module AtprotoAuth
51
65
 
52
66
  def configure
53
67
  yield(configuration)
68
+ configuration.validate!
69
+ configuration
70
+ end
71
+
72
+ # Gets the configured storage backend
73
+ # @return [Storage::Interface] The configured storage implementation
74
+ def storage
75
+ configuration.storage
76
+ end
77
+
78
+ # Resets the configuration to defaults
79
+ # Primarily used in testing
80
+ def reset_configuration!
81
+ @configuration = Configuration.new
54
82
  end
55
83
  end
56
84
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atproto_auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josh Huckabee
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-12-06 00:00:00.000000000 Z
11
+ date: 2024-12-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jose
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '2.9'
41
+ - !ruby/object:Gem::Dependency
42
+ name: redis
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.3'
41
55
  description: A Ruby library for implementing AT Protocol OAuth flows, including DPoP,
42
56
  PAR, and dynamic client registration. Supports both client and server-side implementations
43
57
  with comprehensive security features.
@@ -50,16 +64,19 @@ files:
50
64
  - ".rubocop.yml"
51
65
  - CHANGELOG.md
52
66
  - LICENSE.txt
67
+ - PROJECT_STRUCTURE.txt
53
68
  - README.md
54
69
  - Rakefile
70
+ - examples/confidential_client/.gitignore
55
71
  - examples/confidential_client/Gemfile
56
72
  - examples/confidential_client/Gemfile.lock
57
73
  - examples/confidential_client/README.md
58
74
  - examples/confidential_client/app.rb
59
75
  - examples/confidential_client/config.ru
60
- - examples/confidential_client/config/client-metadata.json
61
- - examples/confidential_client/public/client-metadata.json
76
+ - examples/confidential_client/config/client-metadata.example.json
62
77
  - examples/confidential_client/public/styles.css
78
+ - examples/confidential_client/screenshots/screenshot-1-sign-in.png
79
+ - examples/confidential_client/screenshots/screenshot-2-success.png
63
80
  - examples/confidential_client/scripts/generate_keys.rb
64
81
  - examples/confidential_client/views/authorized.erb
65
82
  - examples/confidential_client/views/index.erb
@@ -72,6 +89,7 @@ files:
72
89
  - lib/atproto_auth/dpop/key_manager.rb
73
90
  - lib/atproto_auth/dpop/nonce_manager.rb
74
91
  - lib/atproto_auth/dpop/proof_generator.rb
92
+ - lib/atproto_auth/encryption.rb
75
93
  - lib/atproto_auth/errors.rb
76
94
  - lib/atproto_auth/http_client.rb
77
95
  - lib/atproto_auth/identity.rb
@@ -83,6 +101,11 @@ files:
83
101
  - lib/atproto_auth/par/request.rb
84
102
  - lib/atproto_auth/par/response.rb
85
103
  - lib/atproto_auth/pkce.rb
104
+ - lib/atproto_auth/serialization/base.rb
105
+ - lib/atproto_auth/serialization/dpop_key.rb
106
+ - lib/atproto_auth/serialization/session.rb
107
+ - lib/atproto_auth/serialization/stored_nonce.rb
108
+ - lib/atproto_auth/serialization/token_set.rb
86
109
  - lib/atproto_auth/server_metadata.rb
87
110
  - lib/atproto_auth/server_metadata/authorization_server.rb
88
111
  - lib/atproto_auth/server_metadata/origin_url.rb
@@ -91,6 +114,11 @@ files:
91
114
  - lib/atproto_auth/state/session.rb
92
115
  - lib/atproto_auth/state/session_manager.rb
93
116
  - lib/atproto_auth/state/token_set.rb
117
+ - lib/atproto_auth/storage/interface.rb
118
+ - lib/atproto_auth/storage/key_builder.rb
119
+ - lib/atproto_auth/storage/memory.rb
120
+ - lib/atproto_auth/storage/redis.rb
121
+ - lib/atproto_auth/token/refresh.rb
94
122
  - lib/atproto_auth/version.rb
95
123
  - sig/atproto_auth.rbs
96
124
  - sig/atproto_auth/client_metadata.rbs