atproto_auth 0.0.1 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtprotoAuth
4
+ module Serialization
5
+ class Error < AtprotoAuth::Error; end
6
+
7
+ class VersionError < Error; end
8
+
9
+ class TypeMismatchError < Error; end
10
+
11
+ class ValidationError < Error; end
12
+
13
+ # Base serializer that all type-specific serializers inherit from
14
+ class Base
15
+ CURRENT_VERSION = 1
16
+ SENSITIVE_FIELDS = [
17
+ "access_token",
18
+ "refresh_token",
19
+ "private_key",
20
+ "d", # EC private key component
21
+ "pkce_verifier"
22
+ ].freeze
23
+
24
+ class << self
25
+ # Serialize object to storage format
26
+ # @param obj [Object] Object to serialize
27
+ # @return [String] JSON serialized data
28
+ def serialize(obj)
29
+ new.serialize(obj)
30
+ end
31
+
32
+ # Deserialize from storage format
33
+ # @param data [String] JSON serialized data
34
+ # @return [Object] Deserialized object
35
+ def deserialize(data)
36
+ new.deserialize(data)
37
+ end
38
+ end
39
+
40
+ def initialize
41
+ @encryption = Encryption::Service.new
42
+ end
43
+
44
+ # Serialize object to storage format
45
+ # @param obj [Object] Object to serialize
46
+ # @return [String] JSON serialized data
47
+ def serialize(obj)
48
+ validate_object!(obj)
49
+
50
+ # First serialize the object
51
+ data = {
52
+ version: CURRENT_VERSION,
53
+ type: type_identifier,
54
+ created_at: Time.now.utc.iso8601,
55
+ updated_at: Time.now.utc.iso8601,
56
+ data: serialize_data(obj)
57
+ }
58
+
59
+ # Then encrypt sensitive fields
60
+ encrypt_sensitive_fields!(data)
61
+
62
+ # Finally convert to JSON
63
+ JSON.generate(data)
64
+ end
65
+
66
+ # Deserialize from storage format
67
+ # @param data [String] JSON serialized data
68
+ # @return [Object] Deserialized object
69
+ def deserialize(data)
70
+ parsed = parse_json(data)
71
+ validate_serialized_data!(parsed)
72
+
73
+ # Decrypt sensitive fields
74
+ decrypt_sensitive_fields!(parsed)
75
+
76
+ # Deserialize according to version
77
+ deserialize_version(parsed)
78
+ end
79
+
80
+ private
81
+
82
+ def encrypt_sensitive_fields!(data, path: [])
83
+ return unless data.is_a?(Hash)
84
+
85
+ data.each do |key, value|
86
+ current_path = path + [key]
87
+
88
+ if sensitive_field?(key)
89
+ data[key] = @encryption.encrypt(
90
+ value.to_s,
91
+ context: current_path.join(".")
92
+ )
93
+ elsif value.is_a?(Hash)
94
+ encrypt_sensitive_fields!(value, path: current_path)
95
+ elsif value.is_a?(Array)
96
+ value.each_with_index do |v, i|
97
+ encrypt_sensitive_fields!(v, path: current_path + [i.to_s]) if v.is_a?(Hash)
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ def decrypt_sensitive_fields!(data, path: [])
104
+ return unless data.is_a?(Hash)
105
+
106
+ data.each do |key, value|
107
+ current_path = path + [key]
108
+ if sensitive_field?(key)
109
+ data[key] = @encryption.decrypt(
110
+ value.transform_keys(&:to_sym),
111
+ context: current_path.join(".")
112
+ )
113
+ elsif value.is_a?(Hash)
114
+ decrypt_sensitive_fields!(value, path: current_path)
115
+ elsif value.is_a?(Array)
116
+ value.each_with_index do |v, i|
117
+ decrypt_sensitive_fields!(v, path: current_path + [i.to_s]) if v.is_a?(Hash)
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ def sensitive_field?(field)
124
+ SENSITIVE_FIELDS.any? do |sensitive_field|
125
+ field.to_s == sensitive_field
126
+ end
127
+ end
128
+
129
+ # Type identifier for serialized data
130
+ # @return [String]
131
+ def type_identifier
132
+ raise NotImplementedError
133
+ end
134
+
135
+ # Serialize object to hash
136
+ # @param obj [Object] Object to serialize
137
+ # @return [Hash] Serialized data
138
+ def serialize_data(_obj)
139
+ raise NotImplementedError
140
+ end
141
+
142
+ # Deserialize object from hash
143
+ # @param data [Hash] Serialized data
144
+ # @return [Object] Deserialized object
145
+ def deserialize_data(_data)
146
+ raise NotImplementedError
147
+ end
148
+
149
+ # Validate object before serialization
150
+ # @param obj [Object] Object to validate
151
+ # @raise [ValidationError] if object is invalid
152
+ def validate_object!(_obj)
153
+ raise NotImplementedError
154
+ end
155
+
156
+ def parse_json(data)
157
+ JSON.parse(data)
158
+ rescue JSON::ParserError => e
159
+ raise Error, "Invalid JSON data: #{e.message}"
160
+ end
161
+
162
+ def validate_serialized_data!(data)
163
+ raise Error, "Invalid serialized data format" unless data.is_a?(Hash)
164
+
165
+ unless data["type"] == type_identifier
166
+ raise TypeMismatchError,
167
+ "Expected type #{type_identifier}, got #{data["type"]}"
168
+ end
169
+
170
+ raise VersionError, "Invalid version format" unless data["version"].is_a?(Integer)
171
+
172
+ return unless data["version"] > CURRENT_VERSION
173
+
174
+ raise VersionError,
175
+ "Version #{data["version"]} not supported"
176
+ end
177
+
178
+ def deserialize_version(data)
179
+ case data["version"]
180
+ when 1
181
+ deserialize_data(data["data"])
182
+ else
183
+ raise VersionError,
184
+ "Unknown version: #{data["version"]}"
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtprotoAuth
4
+ module Serialization
5
+ # Handles serialization of DPoP keys
6
+ class DPoPKey < Base
7
+ def type_identifier
8
+ "DPoPKey"
9
+ end
10
+
11
+ private
12
+
13
+ def validate_object!(obj)
14
+ return if obj.is_a?(AtprotoAuth::DPoP::KeyManager)
15
+
16
+ raise ValidationError,
17
+ "Expected KeyManager object, got #{obj.class}"
18
+ end
19
+
20
+ def serialize_data(obj)
21
+ obj.to_jwk(include_private: true).to_h
22
+ end
23
+
24
+ def deserialize_data(data)
25
+ AtprotoAuth::DPoP::KeyManager.from_jwk(data)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtprotoAuth
4
+ module Serialization
5
+ # Handles serialization of Session objects
6
+ class Session < Base
7
+ def type_identifier
8
+ "Session"
9
+ end
10
+
11
+ private
12
+
13
+ def validate_object!(obj)
14
+ return if obj.is_a?(AtprotoAuth::State::Session)
15
+
16
+ raise ValidationError,
17
+ "Expected Session object, got #{obj.class}"
18
+ end
19
+
20
+ def serialize_data(obj)
21
+ {
22
+ session_id: obj.session_id,
23
+ state_token: obj.state_token,
24
+ client_id: obj.client_id,
25
+ scope: obj.scope,
26
+ pkce_verifier: obj.pkce_verifier,
27
+ pkce_challenge: obj.pkce_challenge,
28
+ did: obj.did,
29
+ tokens: serialize_token_set(obj.tokens),
30
+ auth_server: serialize_auth_server(obj.auth_server)
31
+ }
32
+ end
33
+
34
+ def deserialize_data(data)
35
+ AtprotoAuth::State::Session.new(
36
+ client_id: data["client_id"],
37
+ scope: data["scope"],
38
+ did: data["did"],
39
+ auth_server: deserialize_auth_server(data["auth_server"])
40
+ ).tap do |session|
41
+ # Set readonly attributes
42
+ session.instance_variable_set(:@session_id, data["session_id"])
43
+ session.instance_variable_set(:@state_token, data["state_token"])
44
+ session.instance_variable_set(:@pkce_verifier, data["pkce_verifier"])
45
+ session.instance_variable_set(:@pkce_challenge, data["pkce_challenge"])
46
+
47
+ # Set tokens if present
48
+ session.tokens = deserialize_token_set(data["tokens"]) if data["tokens"]
49
+ end
50
+ end
51
+
52
+ def serialize_token_set(tokens)
53
+ return nil unless tokens
54
+
55
+ TokenSet.new.serialize(tokens)
56
+ end
57
+
58
+ def deserialize_token_set(data)
59
+ return nil unless data
60
+
61
+ TokenSet.new.deserialize(data)
62
+ end
63
+
64
+ def serialize_auth_server(server)
65
+ return nil unless server
66
+
67
+ server.to_h
68
+ end
69
+
70
+ def deserialize_auth_server(data)
71
+ return nil unless data
72
+
73
+ AtprotoAuth::ServerMetadata::AuthorizationServer.new(data)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtprotoAuth
4
+ module Serialization
5
+ # Handles serialization of StoredNonce objects
6
+ class StoredNonce < Base
7
+ def type_identifier
8
+ "StoredNonce"
9
+ end
10
+
11
+ private
12
+
13
+ def validate_object!(obj)
14
+ return if obj.is_a?(AtprotoAuth::DPoP::NonceManager::StoredNonce)
15
+
16
+ raise ValidationError,
17
+ "Expected StoredNonce object, got #{obj.class}"
18
+ end
19
+
20
+ def serialize_data(obj)
21
+ {
22
+ value: obj.value,
23
+ server_url: obj.server_url,
24
+ timestamp: obj.timestamp
25
+ }
26
+ end
27
+
28
+ def deserialize_data(data)
29
+ AtprotoAuth::DPoP::NonceManager::StoredNonce.new(
30
+ data["value"],
31
+ data["server_url"],
32
+ timestamp: Time.at(data["timestamp"])
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtprotoAuth
4
+ module Serialization
5
+ # Handles serialization of TokenSet objects
6
+ class TokenSet < Base
7
+ def type_identifier
8
+ "TokenSet"
9
+ end
10
+
11
+ private
12
+
13
+ def validate_object!(obj)
14
+ return if obj.is_a?(AtprotoAuth::State::TokenSet)
15
+
16
+ raise ValidationError,
17
+ "Expected TokenSet object, got #{obj.class}"
18
+ end
19
+
20
+ def serialize_data(obj)
21
+ {
22
+ access_token: obj.access_token,
23
+ refresh_token: obj.refresh_token,
24
+ token_type: obj.token_type,
25
+ expires_at: obj.expires_at.utc.iso8601,
26
+ scope: obj.scope,
27
+ sub: obj.sub
28
+ }
29
+ end
30
+
31
+ def deserialize_data(data)
32
+ AtprotoAuth::State::TokenSet.new(
33
+ access_token: data["access_token"],
34
+ refresh_token: data["refresh_token"],
35
+ token_type: data["token_type"],
36
+ expires_in: (Time.parse(data["expires_at"]) - Time.now).to_i,
37
+ scope: data["scope"],
38
+ sub: data["sub"]
39
+ )
40
+ end
41
+ end
42
+ end
43
+ end
@@ -57,9 +57,28 @@ module AtprotoAuth
57
57
  new(metadata)
58
58
  end
59
59
 
60
+ def to_h
61
+ {
62
+ issuer: issuer,
63
+ authorization_endpoint: authorization_endpoint,
64
+ token_endpoint: token_endpoint,
65
+ pushed_authorization_request_endpoint: pushed_authorization_request_endpoint,
66
+ response_types_supported: response_types_supported,
67
+ grant_types_supported: grant_types_supported,
68
+ code_challenge_methods_supported: code_challenge_methods_supported,
69
+ token_endpoint_auth_methods_supported: token_endpoint_auth_methods_supported,
70
+ token_endpoint_auth_signing_alg_values_supported: token_endpoint_auth_signing_alg_values_supported,
71
+ scopes_supported: scopes_supported,
72
+ dpop_signing_alg_values_supported: dpop_signing_alg_values_supported,
73
+ authorization_response_iss_parameter_supported: true,
74
+ require_pushed_authorization_requests: true,
75
+ client_id_metadata_document_supported: true
76
+ }
77
+ end
78
+
60
79
  private
61
80
 
62
- def validate_and_set_metadata!(metadata) # rubocop:disable Metrics/AbcSize
81
+ def validate_and_set_metadata!(metadata)
63
82
  REQUIRED_FIELDS.each do |field|
64
83
  raise InvalidAuthorizationServer, "#{field} is required" unless metadata[field]
65
84
  end
@@ -1,18 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "securerandom"
4
- require "time"
5
- require "monitor"
6
-
7
3
  module AtprotoAuth
8
4
  module State
9
- # Manages active OAuth sessions
5
+ # Manages active OAuth sessions with secure persistent storage
10
6
  class SessionManager
11
- include MonitorMixin
12
-
13
7
  def initialize
14
- super # Initialize MonitorMixin
15
- @sessions = {}
8
+ @serializer = Serialization::Session.new
16
9
  end
17
10
 
18
11
  # Creates and stores a new session
@@ -29,8 +22,31 @@ module AtprotoAuth
29
22
  did: did
30
23
  )
31
24
 
32
- synchronize do
33
- @sessions[session.session_id] = session
25
+ # Store both session and state mapping atomically
26
+ session_key = Storage::KeyBuilder.session_key(session.session_id)
27
+ state_key = Storage::KeyBuilder.state_key(session.state_token)
28
+
29
+ AtprotoAuth.storage.with_lock(session_key, ttl: 30) do
30
+ serialized = @serializer.serialize(session)
31
+ AtprotoAuth.storage.set(session_key, serialized)
32
+ AtprotoAuth.storage.set(state_key, session.session_id)
33
+ end
34
+
35
+ session
36
+ end
37
+
38
+ # Updates an existing session
39
+ # @return [Session] The session to update
40
+ # @return [Session] The updated session
41
+ def update_session(session)
42
+ session_key = Storage::KeyBuilder.session_key(session.session_id)
43
+
44
+ AtprotoAuth.storage.with_lock(session_key, ttl: 30) do
45
+ serialized = @serializer.serialize(session)
46
+ AtprotoAuth.storage.set(session_key, serialized)
47
+
48
+ state_key = Storage::KeyBuilder.state_key(session.state_token)
49
+ AtprotoAuth.storage.set(state_key, session.session_id)
34
50
  end
35
51
 
36
52
  session
@@ -40,8 +56,24 @@ module AtprotoAuth
40
56
  # @param session_id [String] Session ID to look up
41
57
  # @return [Session, nil] The session if found
42
58
  def get_session(session_id)
43
- synchronize do
44
- @sessions[session_id]
59
+ session_key = Storage::KeyBuilder.session_key(session_id)
60
+
61
+ begin
62
+ serialized = AtprotoAuth.storage.get(session_key)
63
+ return nil unless serialized
64
+ rescue StandardError => e
65
+ AtprotoAuth.configuration.logger.error("Failed to get session: #{e.message}")
66
+ return nil
67
+ end
68
+
69
+ begin
70
+ session = @serializer.deserialize(serialized)
71
+ return nil if !session.renewable? && session.tokens&.expired?
72
+
73
+ session
74
+ rescue StandardError => e
75
+ AtprotoAuth.configuration.logger.error("Failed to deserialize session: #{e.message}")
76
+ nil
45
77
  end
46
78
  end
47
79
 
@@ -49,26 +81,41 @@ module AtprotoAuth
49
81
  # @param state [String] State token to look up
50
82
  # @return [Session, nil] The session if found
51
83
  def get_session_by_state(state)
52
- synchronize do
53
- @sessions.values.find { |session| session.validate_state(state) }
84
+ return nil unless state
85
+
86
+ state_key = Storage::KeyBuilder.state_key(state)
87
+
88
+ begin
89
+ session_id = AtprotoAuth.storage.get(state_key)
90
+ return nil unless session_id
91
+ rescue StandardError => e
92
+ AtprotoAuth.configuration.logger.error("Failed to get session by state: #{e.message}")
93
+ return nil
54
94
  end
95
+
96
+ get_session(session_id)
55
97
  end
56
98
 
57
99
  # Removes a session
58
100
  # @param session_id [String] Session ID to remove
59
101
  # @return [void]
60
102
  def remove_session(session_id)
61
- synchronize do
62
- @sessions.delete(session_id)
103
+ session = get_session(session_id)
104
+ return unless session
105
+
106
+ session_key = Storage::KeyBuilder.session_key(session_id)
107
+ state_key = Storage::KeyBuilder.state_key(session.state_token)
108
+
109
+ AtprotoAuth.storage.with_lock(session_key, ttl: 30) do
110
+ AtprotoAuth.storage.delete(session_key)
111
+ AtprotoAuth.storage.delete(state_key)
63
112
  end
64
113
  end
65
114
 
66
115
  # Removes all expired sessions
67
116
  # @return [void]
68
117
  def cleanup_expired
69
- synchronize do
70
- @sessions.delete_if { |_, session| !session.renewable? && session.tokens&.expired? }
71
- end
118
+ # No-op - expiry handled by storage TTL and retrieval validation
72
119
  end
73
120
  end
74
121
  end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtprotoAuth
4
+ module Storage
5
+ # Base storage interface that all implementations must conform to
6
+ class Interface
7
+ # Store a value with optional TTL
8
+ # @param key [String] Storage key
9
+ # @param value [Object] Value to store
10
+ # @param ttl [Integer, nil] Time-to-live in seconds
11
+ # @return [Boolean] Success status
12
+ # @raise [StorageError] if operation fails
13
+ def set(key, value, ttl: nil)
14
+ raise NotImplementedError
15
+ end
16
+
17
+ # Retrieve a value
18
+ # @param key [String] Storage key
19
+ # @return [Object, nil] Stored value or nil if not found
20
+ # @raise [StorageError] if operation fails
21
+ def get(key)
22
+ raise NotImplementedError
23
+ end
24
+
25
+ # Delete a value
26
+ # @param key [String] Storage key
27
+ # @return [Boolean] Success status
28
+ # @raise [StorageError] if operation fails
29
+ def delete(key)
30
+ raise NotImplementedError
31
+ end
32
+
33
+ # Check if key exists
34
+ # @param key [String] Storage key
35
+ # @return [Boolean] True if key exists
36
+ # @raise [StorageError] if operation fails
37
+ def exists?(key)
38
+ raise NotImplementedError
39
+ end
40
+
41
+ # Get multiple values
42
+ # @param keys [Array<String>] Storage keys
43
+ # @return [Hash<String, Object>] Key-value pairs
44
+ # @raise [StorageError] if operation fails
45
+ def multi_get(keys)
46
+ raise NotImplementedError
47
+ end
48
+
49
+ # Store multiple values
50
+ # @param hash [Hash<String, Object>] Key-value pairs
51
+ # @param ttl [Integer, nil] Time-to-live in seconds
52
+ # @return [Boolean] Success status
53
+ # @raise [StorageError] if operation fails
54
+ def multi_set(hash, ttl: nil)
55
+ raise NotImplementedError
56
+ end
57
+
58
+ # Acquire a lock
59
+ # @param key [String] Lock key
60
+ # @param ttl [Integer] Lock timeout in seconds
61
+ # @return [Boolean] True if lock acquired
62
+ # @raise [StorageError] if operation fails
63
+ def acquire_lock(key, ttl:)
64
+ raise NotImplementedError
65
+ end
66
+
67
+ # Release a lock
68
+ # @param key [String] Lock key
69
+ # @return [Boolean] Success status
70
+ # @raise [StorageError] if operation fails
71
+ def release_lock(key)
72
+ raise NotImplementedError
73
+ end
74
+
75
+ # Execute block with lock
76
+ # @param key [String] Lock key
77
+ # @param ttl [Integer] Lock timeout in seconds
78
+ # @yield Block to execute with lock
79
+ # @return [Object] Block result
80
+ # @raise [StorageError] if operation fails
81
+ def with_lock(key, ttl: 30)
82
+ raise NotImplementedError
83
+ end
84
+
85
+ protected
86
+
87
+ # Validate key format
88
+ # @param key [String] Key to validate
89
+ # @raise [StorageError] if key is invalid
90
+ def validate_key!(key)
91
+ raise StorageError, "Key cannot be nil" if key.nil?
92
+ raise StorageError, "Key must be a string" unless key.is_a?(String)
93
+ raise StorageError, "Key cannot be empty" if key.empty?
94
+ raise StorageError, "Invalid key format" unless key.start_with?("atproto:")
95
+ end
96
+
97
+ # Validate TTL value
98
+ # @param ttl [Integer, nil] TTL to validate
99
+ # @raise [StorageError] if TTL is invalid
100
+ def validate_ttl!(ttl)
101
+ return if ttl.nil?
102
+ raise StorageError, "TTL must be a positive integer" unless ttl.is_a?(Integer) && ttl.positive?
103
+ end
104
+ end
105
+
106
+ # Base error class for storage operations
107
+ class StorageError < AtprotoAuth::Error; end
108
+
109
+ # Error for lock-related operations
110
+ class LockError < StorageError; end
111
+ end
112
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtprotoAuth
4
+ module Storage
5
+ # Utility for building storage keys with correct format
6
+ class KeyBuilder
7
+ NAMESPACE_SEPARATOR = ":"
8
+ NAMESPACE_PREFIX = "atproto"
9
+
10
+ class << self
11
+ def session_key(id)
12
+ build_key("session", id)
13
+ end
14
+
15
+ def state_key(token)
16
+ build_key("state", token)
17
+ end
18
+
19
+ def nonce_key(server_url)
20
+ build_key("nonce", server_url)
21
+ end
22
+
23
+ def dpop_key(client_id)
24
+ build_key("dpop", client_id)
25
+ end
26
+
27
+ def lock_key(namespace, id)
28
+ build_key("lock", namespace, id)
29
+ end
30
+
31
+ private
32
+
33
+ def build_key(*parts)
34
+ [NAMESPACE_PREFIX, *parts].join(NAMESPACE_SEPARATOR)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end