atproto_auth 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
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