atproto_auth 0.2.3 → 0.2.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50d239692b4a8497da937e550ef8a83e5e02a24c68e605d770ebaef774c585a7
4
- data.tar.gz: 5904cc180dd65f4c50881164afb06b392082e87b6c1e5bd9609d7eabb0d6e057
3
+ metadata.gz: 50b5434e412dc6bd10dfbf53046cfe060110d050540b6b9750f73b473f75471d
4
+ data.tar.gz: d0b259b7e8c2508abdcd5a046e55d1f5aa24f40a1a7bd0e43be9378d7f40aefd
5
5
  SHA512:
6
- metadata.gz: 7605b04694e36a6384210cc2016247f37f4756076d92aaec75e7d6c3788564554a380afad6182aae721c4f175185bbce81eedd15c160102fc9ae5ee150881747
7
- data.tar.gz: 8847fb8b7d3fc9476ab9fadd3870c20e9e06cdd309b8649f256c2dd54b991e495662f9331b8a2602a0abecda34fe77283de0a784c4cd42ff47799024c1077e4f
6
+ metadata.gz: f267e07128db3850494cff1ba08a8678b04a91e6028d43d829eaffcab715a20c57e56b122f116e7ca93814fe4fdf7a16bc7041b1fc09230d8cd325489baa4c4c
7
+ data.tar.gz: efa4d2dfec732cf68eb1b54e0f753355f06abe07f2b3a4e25ad79e4c2bca75dd43a808d34c11484e4d03ecec509986742f8a546cb6ac4deb21705214f55c66df
data/CHANGELOG.md CHANGED
@@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [0.2.1] - 2024-12-11
7
+ ## [0.2.4] - 2024-12-11
8
+ ### Fixed
9
+ - Token refresh now correctly handles expired tokens
10
+
11
+ ## [0.2.2] - 2024-12-11
8
12
  ### Added
9
13
  - Added support for did:web users
10
14
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- atproto_auth (0.2.3)
4
+ atproto_auth (0.2.4)
5
5
  jose (~> 1.2)
6
6
  jwt (~> 2.9)
7
7
  redis (~> 5.3)
@@ -120,24 +120,40 @@ module AtprotoAuth
120
120
  end
121
121
 
122
122
  def request_token_refresh
123
- # Generate DPoP proof for token request
123
+ # Initial token request without nonce
124
+ response = make_token_request(session)
125
+
126
+ # Handle DPoP nonce requirement
127
+ if requires_dpop_nonce?(response)
128
+ # Extract and store nonce from error response
129
+ extract_dpop_nonce(response)
130
+ dpop_client.process_response(response[:headers], auth_server.issuer)
131
+
132
+ # Retry request with nonce
133
+ response = make_token_request(session)
134
+ end
135
+
136
+ handle_refresh_response(response)
137
+ end
138
+
139
+ def make_token_request(session)
140
+ # Generate proof
124
141
  proof = dpop_client.generate_proof(
125
142
  http_method: "POST",
126
143
  http_uri: auth_server.token_endpoint
127
144
  )
128
145
 
129
- # Build refresh request
130
146
  body = {
131
147
  grant_type: "refresh_token",
132
148
  refresh_token: session.tokens.refresh_token,
133
- scope: session.scope
149
+ scope: session.scope,
150
+ client_id: client_metadata.client_id
134
151
  }
135
152
 
136
153
  # Add client authentication if available
137
154
  add_client_authentication!(body) if client_metadata.confidential?
138
155
 
139
- # Make request
140
- response = AtprotoAuth.configuration.http_client.post(
156
+ AtprotoAuth.configuration.http_client.post(
141
157
  auth_server.token_endpoint,
142
158
  body: body,
143
159
  headers: {
@@ -145,16 +161,34 @@ module AtprotoAuth
145
161
  "DPoP" => proof
146
162
  }
147
163
  )
164
+ end
148
165
 
149
- handle_refresh_response(response)
166
+ def requires_dpop_nonce?(response)
167
+ return false unless response[:status] == 400
168
+
169
+ error_data = JSON.parse(response[:body])
170
+ error_data["error"] == "use_dpop_nonce"
171
+ rescue JSON::ParserError
172
+ false
173
+ end
174
+
175
+ def extract_dpop_nonce(response)
176
+ headers = response[:headers]
177
+ nonce = headers["DPoP-Nonce"] ||
178
+ headers["dpop-nonce"] ||
179
+ headers["Dpop-Nonce"]
180
+
181
+ raise TokenError, "No DPoP nonce provided in error response" unless nonce
182
+
183
+ nonce
150
184
  end
151
185
 
152
186
  def add_client_authentication!(body)
153
- return unless session.client_metadata.jwks && !session.client_metadata.jwks["keys"].empty?
187
+ return unless client_metadata.jwks && !client_metadata.jwks["keys"].empty?
154
188
 
155
- signing_key = JOSE::JWK.from_map(session.client_metadata.jwks["keys"].first)
189
+ signing_key = JOSE::JWK.from_map(client_metadata.jwks["keys"].first)
156
190
  client_assertion = PAR::ClientAssertion.new(
157
- client_id: session.client_metadata.client_id,
191
+ client_id: client_metadata.client_id,
158
192
  signing_key: signing_key
159
193
  )
160
194
 
@@ -194,25 +228,31 @@ module AtprotoAuth
194
228
  sub: data["sub"]
195
229
  )
196
230
  rescue JSON::ParserError => e
197
- raise RefreshError, "Invalid response format: #{e.message}"
231
+ raise TokenError, "Invalid response format: #{e.message}"
198
232
  end
199
233
 
200
234
  def handle_400_response(response)
201
235
  error_data = JSON.parse(response[:body])
202
236
  error_description = error_data["error_description"] || error_data["error"]
203
237
 
204
- # Handle DPoP nonce requirement
205
- if error_data["error"] == "use_dpop_nonce"
238
+ case error_data["error"]
239
+ when "use_dpop_nonce"
206
240
  dpop_client.process_response(response[:headers], auth_server.issuer)
207
- raise RefreshError, "Retry with DPoP nonce"
241
+ raise TokenError.new("Retry with DPoP nonce", retry_possible: true)
242
+ when "invalid_grant"
243
+ # The refresh token has been invalidated or already used
244
+ raise TokenError.new(
245
+ "Refresh token has been invalidated: #{error_description}",
246
+ retry_possible: false
247
+ )
248
+ else
249
+ raise TokenError.new(
250
+ "Refresh request failed: #{error_description}",
251
+ retry_possible: false
252
+ )
208
253
  end
209
-
210
- raise RefreshError.new(
211
- "Refresh request failed: #{error_description}",
212
- retry_possible: false
213
- )
214
254
  rescue JSON::ParserError
215
- raise RefreshError, "Invalid error response format"
255
+ raise TokenError, "Invalid error response format"
216
256
  end
217
257
 
218
258
  def handle_rate_limit_response(response)
@@ -224,25 +264,25 @@ module AtprotoAuth
224
264
  def validate_refresh_response!(data)
225
265
  # Required fields
226
266
  %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]
267
+ raise TokenError.new("Missing #{field} in response", retry_possible: false) unless data[field]
228
268
  end
229
269
 
230
270
  # Token type must be DPoP
231
271
  unless data["token_type"] == "DPoP"
232
- raise RefreshError.new("Invalid token_type: #{data["token_type"]}", retry_possible: false)
272
+ raise TokenError.new("Invalid token_type: #{data["token_type"]}", retry_possible: false)
233
273
  end
234
274
 
235
275
  # Scope must include original scopes
236
276
  original_scopes = session.scope.split
237
277
  response_scopes = data["scope"].split
238
278
  unless (original_scopes - response_scopes).empty?
239
- raise RefreshError.new("Invalid scope in response", retry_possible: false)
279
+ raise TokenError.new("Invalid scope in response", retry_possible: false)
240
280
  end
241
281
 
242
282
  # Subject must match
243
283
  return if data["sub"] == session.tokens.sub
244
284
 
245
- raise RefreshError.new("Subject mismatch in response", retry_possible: false)
285
+ raise TokenError.new("Subject mismatch in response", retry_possible: false)
246
286
  end
247
287
  end
248
288
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtprotoAuth
4
- VERSION = "0.2.3"
4
+ VERSION = "0.2.4"
5
5
  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.2.3
4
+ version: 0.2.4
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-12 00:00:00.000000000 Z
11
+ date: 2024-12-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jose