atproto_auth 0.0.1 → 0.1.1

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/README.md +91 -5
  5. data/examples/confidential_client/.gitignore +2 -0
  6. data/examples/confidential_client/Gemfile +1 -0
  7. data/examples/confidential_client/Gemfile.lock +10 -1
  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 -5
  39. data/examples/confidential_client/config/client-metadata.json +0 -25
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize, Metrics/CyclomaticComplexity
4
-
5
3
  module AtprotoAuth
6
4
  # Main client class for AT Protocol OAuth implementation. Handles the complete
7
5
  # OAuth flow including authorization, token management, and identity verification.
@@ -12,6 +10,16 @@ module AtprotoAuth
12
10
  # Error raised when token operations fail
13
11
  class TokenError < Error; end
14
12
 
13
+ # Error raised when session operations fail
14
+ class SessionError < Error
15
+ attr_reader :session_id
16
+
17
+ def initialize(message, session_id: nil)
18
+ @session_id = session_id
19
+ super(message)
20
+ end
21
+ end
22
+
15
23
  # @return [String] OAuth client ID
16
24
  attr_reader :client_id
17
25
  # @return [String] OAuth redirect URI
@@ -59,6 +67,9 @@ module AtprotoAuth
59
67
  scope: scope
60
68
  )
61
69
 
70
+ # Store session with storage backend
71
+ session_manager.update_session(session)
72
+
62
73
  # Resolve identity and authorization server if handle provided
63
74
  if handle
64
75
  auth_info = resolve_from_handle(handle, session)
@@ -96,34 +107,39 @@ module AtprotoAuth
96
107
  # Verify issuer matches session
97
108
  raise CallbackError, "Issuer mismatch" unless session.auth_server && session.auth_server.issuer == iss
98
109
 
99
- # Exchange code for tokens
100
- token_response = exchange_code(
101
- code: code,
102
- session: session
103
- )
104
-
105
- # Validate token response
106
- validate_token_response!(token_response, session)
107
-
108
- # Create token set and store in session
109
- token_set = State::TokenSet.new(
110
- access_token: token_response["access_token"],
111
- token_type: token_response["token_type"],
112
- expires_in: token_response["expires_in"],
113
- refresh_token: token_response["refresh_token"],
114
- scope: token_response["scope"],
115
- sub: token_response["sub"]
116
- )
117
- session.tokens = token_set
110
+ AtprotoAuth.storage.with_lock(Storage::KeyBuilder.lock_key("session", session.session_id), ttl: 30) do
111
+ # Exchange code for tokens
112
+ token_response = exchange_code(
113
+ code: code,
114
+ session: session
115
+ )
118
116
 
119
- {
120
- access_token: token_set.access_token,
121
- token_type: token_set.token_type,
122
- expires_in: (token_set.expires_at - Time.now).to_i,
123
- refresh_token: token_set.refresh_token,
124
- scope: token_set.scope,
125
- session_id: session.session_id
126
- }
117
+ # Validate token response
118
+ validate_token_response!(token_response, session)
119
+
120
+ # Create token set and store in session
121
+ token_set = State::TokenSet.new(
122
+ access_token: token_response["access_token"],
123
+ token_type: token_response["token_type"],
124
+ expires_in: token_response["expires_in"],
125
+ refresh_token: token_response["refresh_token"],
126
+ scope: token_response["scope"],
127
+ sub: token_response["sub"]
128
+ )
129
+ session.tokens = token_set
130
+
131
+ # Update stored session
132
+ session_manager.update_session(session)
133
+
134
+ {
135
+ access_token: token_set.access_token,
136
+ token_type: token_set.token_type,
137
+ expires_in: (token_set.expires_at - Time.now).to_i,
138
+ refresh_token: token_set.refresh_token,
139
+ scope: token_set.scope,
140
+ session_id: session.session_id
141
+ }
142
+ end
127
143
  end
128
144
 
129
145
  # Gets active tokens for a session
@@ -142,6 +158,38 @@ module AtprotoAuth
142
158
  }
143
159
  end
144
160
 
161
+ # Refreshes tokens for a session
162
+ # @param session_id [String] ID of session to refresh
163
+ def refresh_token(session_id)
164
+ session = session_manager.get_session(session_id)
165
+ raise TokenError, "Invalid session" unless session
166
+ raise TokenError, "Session not authorized" unless session.renewable?
167
+
168
+ AtprotoAuth.storage.with_lock(Storage::KeyBuilder.lock_key("session", session.session_id), ttl: 30) do
169
+ refresher = Token::Refresh.new(
170
+ session: session,
171
+ dpop_client: @dpop_client,
172
+ auth_server: session.auth_server,
173
+ client_metadata: client_metadata
174
+ )
175
+
176
+ new_tokens = refresher.perform!
177
+ session.tokens = new_tokens
178
+
179
+ # Update stored session
180
+ session_manager.update_session(session)
181
+
182
+ {
183
+ access_token: new_tokens.access_token,
184
+ token_type: new_tokens.token_type,
185
+ expires_in: (new_tokens.expires_at - Time.now).to_i,
186
+ refresh_token: new_tokens.refresh_token,
187
+ scope: new_tokens.scope,
188
+ session_id: session.session_id
189
+ }
190
+ end
191
+ end
192
+
145
193
  # Checks if a session has valid tokens
146
194
  # @param session_id [String] ID of session to check
147
195
  # @return [Boolean] true if session exists and has valid tokens
@@ -174,6 +222,21 @@ module AtprotoAuth
174
222
  }
175
223
  end
176
224
 
225
+ # Removes a session and its stored data
226
+ # @param session_id [String] ID of session to remove
227
+ # @return [void]
228
+ def remove_session(session_id)
229
+ key = Storage::KeyBuilder.session_key(session_id)
230
+ AtprotoAuth.storage.delete(key)
231
+ session_manager.remove_session(session_id)
232
+ end
233
+
234
+ # Cleans up expired sessions from storage
235
+ # @return [void]
236
+ def cleanup_expired_sessions
237
+ session_manager.cleanup_expired
238
+ end
239
+
177
240
  private
178
241
 
179
242
  def load_client_metadata(metadata)
@@ -214,6 +277,9 @@ module AtprotoAuth
214
277
  server = resolve_auth_server(resolution[:pds])
215
278
  session.authorization_server = server
216
279
 
280
+ # Update stored session
281
+ session_manager.update_session(session)
282
+
217
283
  { server: server, pds: resolution[:pds] }
218
284
  end
219
285
 
@@ -222,6 +288,9 @@ module AtprotoAuth
222
288
  server = resolve_auth_server(pds_url)
223
289
  session.authorization_server = server
224
290
 
291
+ # Update stored session
292
+ session_manager.update_session(session)
293
+
225
294
  { server: server, pds: pds_url }
226
295
  end
227
296
 
@@ -296,8 +365,6 @@ module AtprotoAuth
296
365
 
297
366
  # Handle DPoP nonce requirement
298
367
  if requires_dpop_nonce?(response)
299
- puts "*" * 88
300
- puts "requires_dpop_nonce"
301
368
  # Extract and store nonce from error response
302
369
  extract_dpop_nonce(response)
303
370
  dpop_client.process_response(response[:headers], session.auth_server.issuer)
@@ -322,11 +389,6 @@ module AtprotoAuth
322
389
  http_uri: session.auth_server.token_endpoint
323
390
  )
324
391
 
325
- # Log the DPoP proof details
326
- AtprotoAuth.configuration.logger.debug "Token Request DPoP Proof:"
327
- AtprotoAuth.configuration.logger.debug "- Key: #{dpop_client.public_key}"
328
- AtprotoAuth.configuration.logger.debug "- Proof: #{proof}"
329
-
330
392
  body = {
331
393
  grant_type: "authorization_code",
332
394
  code: code,
@@ -406,5 +468,3 @@ module AtprotoAuth
406
468
  end
407
469
  end
408
470
  end
409
-
410
- # rubocop:enable Metrics/PerceivedComplexity, Metrics/AbcSize, Metrics/CyclomaticComplexity
@@ -56,7 +56,7 @@ module AtprotoAuth
56
56
 
57
57
  private
58
58
 
59
- def validate_and_set_metadata!(metadata) # rubocop:disable Metrics/AbcSize
59
+ def validate_and_set_metadata!(metadata)
60
60
  # Required fields
61
61
  @application_type = validate_application_type(metadata["application_type"])
62
62
  @client_id = validate_client_id!(metadata["client_id"])
@@ -208,7 +208,7 @@ module AtprotoAuth
208
208
  end
209
209
  end
210
210
 
211
- def validate_auth_methods!(metadata) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
211
+ def validate_auth_methods!(metadata)
212
212
  @token_endpoint_auth_method = metadata["token_endpoint_auth_method"]
213
213
  return unless @token_endpoint_auth_method == "private_key_jwt"
214
214
 
@@ -3,15 +3,49 @@
3
3
  require "logger"
4
4
 
5
5
  module AtprotoAuth
6
+ class ConfigurationError < Error; end
7
+
6
8
  # Configuration class for global AtprotoAuth settings
7
9
  class Configuration
8
- attr_accessor :default_token_lifetime, :dpop_nonce_lifetime, :http_client, :logger
10
+ attr_accessor :default_token_lifetime,
11
+ :dpop_nonce_lifetime,
12
+ :encryption,
13
+ :http_client,
14
+ :logger,
15
+ :storage
9
16
 
10
17
  def initialize
11
18
  @default_token_lifetime = 300 # 5 minutes in seconds
12
19
  @dpop_nonce_lifetime = 300 # 5 minutes in seconds
20
+ @encryption = nil
13
21
  @http_client = nil
14
22
  @logger = Logger.new($stdout)
23
+ @storage = AtprotoAuth::Storage::Memory.new
24
+ end
25
+
26
+ # Validates the current configuration
27
+ # @raise [ConfigurationError] if configuration is invalid
28
+ def validate!
29
+ validate_storage!
30
+ validate_http_client!
31
+ true
32
+ end
33
+
34
+ private
35
+
36
+ def validate_storage!
37
+ raise ConfigurationError, "Storage must be configured" if @storage.nil?
38
+ return if @storage.is_a?(AtprotoAuth::Storage::Interface)
39
+
40
+ raise ConfigurationError, "Storage must implement Storage::Interface"
41
+ end
42
+
43
+ def validate_http_client!
44
+ return if @http_client.nil? # Allow nil for testing
45
+
46
+ return if @http_client.respond_to?(:get) && @http_client.respond_to?(:post)
47
+
48
+ raise ConfigurationError, "HTTP client must implement get and post methods"
15
49
  end
16
50
  end
17
51
  end
@@ -189,7 +189,7 @@ module AtprotoAuth
189
189
  asn1_to_raw(signature)
190
190
  end
191
191
 
192
- def extract_ec_key # rubocop:disable Metrics/AbcSize
192
+ def extract_ec_key
193
193
  # Extract the raw EC key from JOSE JWK
194
194
  key_data = @keypair.to_map
195
195
  raise KeyError, "Private key required for signing" unless key_data["d"] # Private key component
@@ -1,40 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "monitor"
4
-
5
3
  module AtprotoAuth
6
4
  module DPoP
7
5
  # Manages DPoP nonces provided by servers during the OAuth flow.
8
- # Tracks separate nonces for Resource Server and Authorization Server.
6
+ # Tracks separate nonces for each server using persistent storage.
9
7
  # Thread-safe to handle concurrent requests.
10
8
  class NonceManager
11
9
  # Error for nonce-related issues
12
10
  class NonceError < AtprotoAuth::Error; end
13
11
 
14
- # Represents a stored nonce with its timestamp
12
+ # Represents a stored nonce with its server URL
15
13
  class StoredNonce
16
- attr_reader :value, :timestamp, :server_url
14
+ attr_reader :value, :server_url, :timestamp
17
15
 
18
- def initialize(value, server_url)
16
+ def initialize(value, server_url, timestamp: nil)
19
17
  @value = value
20
18
  @server_url = server_url
21
- @timestamp = Time.now.to_i
22
- end
23
-
24
- def expired?(ttl = nil)
25
- return false unless ttl
26
-
27
- (Time.now.to_i - @timestamp) > ttl
19
+ @timestamp = timestamp || Time.now.to_i
28
20
  end
29
21
  end
30
22
 
31
- # Maximum time in seconds a nonce is considered valid
23
+ # Default time in seconds a nonce is considered valid
32
24
  DEFAULT_TTL = 300 # 5 minutes
33
25
 
34
26
  def initialize(ttl: nil)
35
27
  @ttl = ttl || DEFAULT_TTL
36
- @nonces = {}
37
- @monitor = Monitor.new
28
+ @serializer = Serialization::StoredNonce.new
38
29
  end
39
30
 
40
31
  # Updates the stored nonce for a server
@@ -45,9 +36,13 @@ module AtprotoAuth
45
36
  validate_inputs!(nonce, server_url)
46
37
  origin = normalize_server_url(server_url)
47
38
 
48
- @monitor.synchronize do
49
- @nonces[origin] = StoredNonce.new(nonce, origin)
50
- end
39
+ stored_nonce = StoredNonce.new(nonce, origin)
40
+ serialized = @serializer.serialize(stored_nonce)
41
+
42
+ key = Storage::KeyBuilder.nonce_key(origin)
43
+ return if AtprotoAuth.storage.set(key, serialized, ttl: @ttl)
44
+
45
+ raise NonceError, "Failed to store nonce"
51
46
  end
52
47
 
53
48
  # Gets the current nonce for a server
@@ -57,36 +52,26 @@ module AtprotoAuth
57
52
  def get(server_url)
58
53
  validate_server_url!(server_url)
59
54
  origin = normalize_server_url(server_url)
55
+ key = Storage::KeyBuilder.nonce_key(origin)
60
56
 
61
- @monitor.synchronize do
62
- stored = @nonces[origin]
63
- return nil if stored.nil? || stored.expired?(@ttl)
57
+ stored = AtprotoAuth.storage.get(key)
58
+ return nil unless stored
64
59
 
65
- stored.value
60
+ begin
61
+ stored_nonce = @serializer.deserialize(stored)
62
+ stored_nonce.value
63
+ rescue Serialization::Error => e
64
+ raise NonceError, "Failed to deserialize nonce: #{e.message}"
66
65
  end
67
66
  end
68
67
 
69
- # Clears an expired nonce for a server
68
+ # Clears a nonce for a server
70
69
  # @param server_url [String] The server's URL
71
70
  def clear(server_url)
72
- @monitor.synchronize do
73
- @nonces.delete(server_url)
74
- end
75
- end
76
-
77
- # Clears all stored nonces
78
- def clear_all
79
- @monitor.synchronize do
80
- @nonces.clear
81
- end
82
- end
83
-
84
- # Get all currently stored server URLs
85
- # @return [Array<String>] Array of server URLs with stored nonces
86
- def server_urls
87
- @monitor.synchronize do
88
- @nonces.keys
89
- end
71
+ validate_server_url!(server_url)
72
+ origin = normalize_server_url(server_url)
73
+ key = Storage::KeyBuilder.nonce_key(origin)
74
+ AtprotoAuth.storage.delete(key)
90
75
  end
91
76
 
92
77
  # Check if a server has a valid nonce
@@ -94,11 +79,9 @@ module AtprotoAuth
94
79
  # @return [Boolean] true if server has a valid nonce
95
80
  def valid_nonce?(server_url)
96
81
  validate_server_url!(server_url)
97
-
98
- @monitor.synchronize do
99
- stored = @nonces[server_url]
100
- !stored.nil? && !stored.expired?(@ttl)
101
- end
82
+ origin = normalize_server_url(server_url)
83
+ key = Storage::KeyBuilder.nonce_key(origin)
84
+ AtprotoAuth.storage.exists?(key)
102
85
  end
103
86
 
104
87
  private
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/atproto_auth/encryption.rb
4
+ module AtprotoAuth
5
+ module Encryption
6
+ class Error < AtprotoAuth::Error; end
7
+ class ConfigurationError < Error; end
8
+ class EncryptionError < Error; end
9
+ class DecryptionError < Error; end
10
+
11
+ # HKDF implementation based on RFC 5869
12
+ module HKDF
13
+ def self.derive(secret, salt:, info:, length:)
14
+ # 1. extract
15
+ prk = OpenSSL::HMAC.digest(
16
+ OpenSSL::Digest.new("SHA256"),
17
+ salt.empty? ? "\x00" * 32 : salt,
18
+ secret.to_s
19
+ )
20
+
21
+ # 2. expand
22
+ n = (length.to_f / 32).ceil
23
+ t = [""]
24
+ output = ""
25
+ 1.upto(n) do |i|
26
+ t[i] = OpenSSL::HMAC.digest(
27
+ OpenSSL::Digest.new("SHA256"),
28
+ prk,
29
+ t[i - 1] + info + [i].pack("C")
30
+ )
31
+ output += t[i]
32
+ end
33
+ output[0, length]
34
+ end
35
+ end
36
+
37
+ # Core encryption service - used internally by serializers
38
+ class Service
39
+ CIPHER = "aes-256-gcm"
40
+ VERSION = 1
41
+
42
+ def initialize
43
+ @key_provider = KeyProvider.new
44
+ end
45
+
46
+ def encrypt(data, context:)
47
+ validate_encryption_inputs!(data, context)
48
+
49
+ iv = SecureRandom.random_bytes(12)
50
+
51
+ cipher = OpenSSL::Cipher.new(CIPHER)
52
+ cipher.encrypt
53
+ cipher.key = @key_provider.key_for_context(context)
54
+ cipher.iv = iv
55
+ cipher.auth_data = context.to_s
56
+
57
+ encrypted = cipher.update(data.to_s) + cipher.final
58
+ auth_tag = cipher.auth_tag
59
+
60
+ {
61
+ version: VERSION,
62
+ iv: Base64.strict_encode64(iv),
63
+ data: Base64.strict_encode64(encrypted),
64
+ tag: Base64.strict_encode64(auth_tag)
65
+ }
66
+ rescue StandardError => e
67
+ raise EncryptionError, "Encryption failed: #{e.message}"
68
+ end
69
+
70
+ def decrypt(encrypted, context:)
71
+ validate_decryption_inputs!(encrypted, context)
72
+ validate_encrypted_data!(encrypted)
73
+
74
+ iv = Base64.strict_decode64(encrypted[:iv])
75
+ data = Base64.strict_decode64(encrypted[:data])
76
+ auth_tag = Base64.strict_decode64(encrypted[:tag])
77
+
78
+ cipher = OpenSSL::Cipher.new(CIPHER)
79
+ cipher.decrypt
80
+ cipher.key = @key_provider.key_for_context(context)
81
+ cipher.iv = iv
82
+ cipher.auth_tag = auth_tag
83
+ cipher.auth_data = context.to_s
84
+
85
+ cipher.update(data) + cipher.final
86
+ rescue ArgumentError => e
87
+ raise DecryptionError, "Invalid encrypted data format: #{e.message}"
88
+ rescue StandardError => e
89
+ raise DecryptionError, "Decryption failed: #{e.message}"
90
+ end
91
+
92
+ private
93
+
94
+ def validate_encryption_inputs!(data, context)
95
+ raise EncryptionError, "Data cannot be nil" if data.nil?
96
+ raise EncryptionError, "Context cannot be nil" if context.nil?
97
+ raise EncryptionError, "Context must be a string" unless context.is_a?(String)
98
+ raise EncryptionError, "Context cannot be empty" if context.empty?
99
+ end
100
+
101
+ def validate_decryption_inputs!(encrypted, context)
102
+ raise DecryptionError, "Encrypted data cannot be nil" if encrypted.nil?
103
+ raise DecryptionError, "Context cannot be nil" if context.nil?
104
+ raise DecryptionError, "Context must be a string" unless context.is_a?(String)
105
+ raise DecryptionError, "Context cannot be empty" if context.empty?
106
+ end
107
+
108
+ def validate_encrypted_data!(encrypted)
109
+ raise DecryptionError, "Invalid encrypted data format" unless encrypted.is_a?(Hash)
110
+
111
+ unless encrypted[:version] == VERSION
112
+ raise DecryptionError, "Unsupported encryption version: #{encrypted[:version]}"
113
+ end
114
+
115
+ %i[iv data tag].each do |field|
116
+ raise DecryptionError, "Missing required field: #{field}" unless encrypted[field].is_a?(String)
117
+ end
118
+ end
119
+ end
120
+
121
+ # Handles key management and derivation
122
+ class KeyProvider
123
+ def initialize
124
+ @master_key = load_master_key
125
+ end
126
+
127
+ def key_for_context(context)
128
+ raise ConfigurationError, "Context is required" if context.nil?
129
+
130
+ HKDF.derive(
131
+ @master_key,
132
+ salt: salt_for_context(context),
133
+ info: "atproto-#{context}",
134
+ length: 32
135
+ )
136
+ end
137
+
138
+ private
139
+
140
+ def load_master_key
141
+ # Try environment variable first
142
+ key = ENV.fetch("ATPROTO_MASTER_KEY", nil)
143
+ return Base64.strict_decode64(key) if key
144
+
145
+ # Generate and store a random key if not configured
146
+ key = SecureRandom.random_bytes(32)
147
+ warn "WARNING: Using randomly generated encryption key - tokens will not persist across restarts"
148
+ key
149
+ end
150
+
151
+ def salt_for_context(context)
152
+ OpenSSL::Digest.digest("SHA256", "atproto-salt-#{context}")
153
+ end
154
+ end
155
+ end
156
+ end
@@ -136,7 +136,7 @@ module AtprotoAuth
136
136
  raise HttpError.new("Request failed: #{e.message}", nil)
137
137
  end
138
138
 
139
- def make_post_request(uri, body, headers = {}, redirect_count = 0) # rubocop:disable Metrics/AbcSize
139
+ def make_post_request(uri, body, headers = {}, redirect_count = 0)
140
140
  http = Net::HTTP.new(uri.host, uri.port)
141
141
  configure_http_client!(http)
142
142
 
@@ -182,7 +182,7 @@ module AtprotoAuth
182
182
  # - headers: Hash of headers from the original request
183
183
  # - redirect_count: Number of redirects so far
184
184
  # - body: Request body for POST requests
185
- def handle_redirect(**kwargs) # rubocop:disable Metrics/AbcSize
185
+ def handle_redirect(**kwargs)
186
186
  response = kwargs[:response]
187
187
  redirect_count = kwargs[:redirect_count]
188
188
 
@@ -78,7 +78,7 @@ module AtprotoAuth
78
78
  raise DocumentError, "Invalid DID format (must be did:plc:): #{did}"
79
79
  end
80
80
 
81
- def validate_services!(services) # rubocop:disable Metrics/CyclomaticComplexity
81
+ def validate_services!(services)
82
82
  return if services.nil?
83
83
  raise DocumentError, "services must be an array" unless services.is_a?(Array)
84
84
 
@@ -119,7 +119,7 @@ module AtprotoAuth
119
119
  normalized.downcase
120
120
  end
121
121
 
122
- def resolve_handle_dns(handle) # rubocop:disable Metrics/CyclomaticComplexity
122
+ def resolve_handle_dns(handle)
123
123
  domain = extract_domain(handle)
124
124
  return nil unless domain
125
125