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 +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
|