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 +4 -4
- data/CHANGELOG.md +5 -1
- data/examples/confidential_client/Gemfile.lock +1 -1
- data/lib/atproto_auth/token/refresh.rb +63 -23
- data/lib/atproto_auth/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 50b5434e412dc6bd10dfbf53046cfe060110d050540b6b9750f73b473f75471d
|
4
|
+
data.tar.gz: d0b259b7e8c2508abdcd5a046e55d1f5aa24f40a1a7bd0e43be9378d7f40aefd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
|
@@ -120,24 +120,40 @@ module AtprotoAuth
|
|
120
120
|
end
|
121
121
|
|
122
122
|
def request_token_refresh
|
123
|
-
#
|
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
|
-
|
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
|
-
|
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
|
187
|
+
return unless client_metadata.jwks && !client_metadata.jwks["keys"].empty?
|
154
188
|
|
155
|
-
signing_key = JOSE::JWK.from_map(
|
189
|
+
signing_key = JOSE::JWK.from_map(client_metadata.jwks["keys"].first)
|
156
190
|
client_assertion = PAR::ClientAssertion.new(
|
157
|
-
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
|
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
|
-
|
205
|
-
|
238
|
+
case error_data["error"]
|
239
|
+
when "use_dpop_nonce"
|
206
240
|
dpop_client.process_response(response[:headers], auth_server.issuer)
|
207
|
-
raise
|
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
|
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
|
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
|
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
|
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
|
285
|
+
raise TokenError.new("Subject mismatch in response", retry_possible: false)
|
246
286
|
end
|
247
287
|
end
|
248
288
|
end
|
data/lib/atproto_auth/version.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2024-12-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: jose
|