authproto 0.3.0.beta2 → 0.3.0.beta3
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 +19 -0
- data/README.md +108 -5
- data/examples/confidential_client/README.md +10 -0
- data/examples/confidential_client/app.rb +8 -7
- data/examples/confidential_client/scripts/generate_keys.rb +1 -1
- data/examples/minisky_console/Gemfile.lock +2 -2
- data/examples/minisky_console/README.md +20 -1
- data/examples/minisky_console/localhost.rb +5 -3
- data/examples/minisky_console/production.rb +9 -3
- data/lib/authproto/client.rb +73 -90
- data/lib/authproto/configuration.rb +37 -16
- data/lib/authproto/dpop/client.rb +105 -40
- data/lib/authproto/dpop/{key_manager.rb → key_set.rb} +54 -120
- data/lib/authproto/dpop/nonce_manager.rb +33 -20
- data/lib/authproto/dpop/proof_generator.rb +17 -32
- data/lib/authproto/dpop.rb +19 -0
- data/lib/authproto/http_helpers.rb +85 -0
- data/lib/authproto/metadata/authorization_server.rb +12 -6
- data/lib/authproto/metadata/client_metadata.rb +18 -5
- data/lib/authproto/metadata/resource_server.rb +4 -2
- data/lib/authproto/minisky.rb +36 -34
- data/lib/authproto/par/client.rb +22 -73
- data/lib/authproto/par/client_assertion.rb +47 -16
- data/lib/authproto/par/request.rb +4 -8
- data/lib/authproto/par.rb +2 -1
- data/lib/authproto/serialization/session.rb +31 -13
- data/lib/authproto/serialization/token_set.rb +1 -2
- data/lib/authproto/state/session.rb +66 -6
- data/lib/authproto/state/session_manager.rb +95 -21
- data/lib/authproto/state/token_set.rb +16 -16
- data/lib/authproto/state.rb +1 -1
- data/lib/authproto/storage/key_builder.rb +3 -11
- data/lib/authproto/storage/memory.rb +8 -2
- data/lib/authproto/token/refresh.rb +26 -59
- data/lib/authproto/validated_uri.rb +23 -0
- data/lib/authproto/version.rb +1 -1
- data/lib/authproto.rb +0 -1
- data/sig/authproto/client.rbs +5 -7
- data/sig/authproto/dependencies.rbs +2 -0
- data/sig/authproto/dpop/client.rbs +11 -4
- data/sig/authproto/dpop/{key_manager.rbs → key_set.rbs} +9 -11
- data/sig/authproto/dpop/nonce_manager.rbs +4 -4
- data/sig/authproto/dpop/proof_generator.rbs +5 -10
- data/sig/authproto/dpop.rbs +6 -0
- data/sig/authproto/http_helpers.rbs +7 -0
- data/sig/authproto/localhost_server.rbs +1 -1
- data/sig/authproto/metadata/authorization_server.rbs +1 -0
- data/sig/authproto/metadata/client_metadata.rbs +2 -1
- data/sig/authproto/metadata/resource_server.rbs +1 -0
- data/sig/authproto/minisky_oauth.rbs +5 -2
- data/sig/authproto/par/client.rbs +4 -8
- data/sig/authproto/par/client_assertion.rbs +4 -0
- data/sig/authproto/par/request.rbs +0 -9
- data/sig/authproto/state/session.rbs +10 -3
- data/sig/authproto/state/session_manager.rbs +8 -3
- data/sig/authproto/state/token_set.rbs +1 -2
- data/sig/authproto/storage/key_builder.rbs +1 -2
- data/sig/authproto/token/refresh.rbs +3 -4
- data/sig/authproto.rbs +5 -2
- metadata +14 -19
- data/.rspec +0 -1
- data/.rubocop.yml +0 -31
- data/.tm_properties +0 -1
- data/.yardopts +0 -5
- data/AGENTS.md +0 -80
- data/Rakefile +0 -8
- data/examples/confidential_client/.gitignore +0 -6
- data/examples/minisky_console/.gitignore +0 -2
- data/lib/authproto/serialization/dpop_key.rb +0 -38
- data/misc/yard_attributes.rb +0 -34
- data/sig/authproto/serialization/dpop_key.rbs +0 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 61754b5e7342299c331cabc76c9585ef1f7b1398d0eae4eed252f5d9a2a019bd
|
|
4
|
+
data.tar.gz: 5879610764cb56945d3fc260ed0538f5cdaaabfaec4982af38408290a34a547f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fe5452fe5a70e70bb3c100b30656eb63fcca61dec4617ccec1413eff03e952156523f39f479096dc2fa23467ad7fa792639571ce5d46a9985049bbb7c0806e82
|
|
7
|
+
data.tar.gz: 2c6fb2275d03bf3b812a731841fe2b7364d41b8799f5e937a4c4191d7da0b843d68bd5d67a941dea4f6d4b0590320cd757041f518a3c1416000888ca165583d5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## [0.3.0.beta3] - 2026-11-02
|
|
2
|
+
|
|
3
|
+
- session's state token is deleted on successfully completed authentication (i.e. `handle_callback`)
|
|
4
|
+
- pending sessions (that haven't completed authentication) are stored with a TTL of 15 minutes by default, and expire after that; this can be changed through `pending_session_ttl` in the global configuration
|
|
5
|
+
- completed but non-renewable session (without a refresh token) are set with a TTL such that they expire when their access token expires
|
|
6
|
+
- fixed deleting a session's state token in `Client#delete_session`
|
|
7
|
+
- DPoP keys are now connected to a specific session and saved & restored with it; multiple sessions used from the same `AuthProto::Client` will have proofs signed with different DPoP keys, and the keys are automatically restored and don't need to be passed in the argument to `Client` or `Minisky::OAuthClient`
|
|
8
|
+
- DPoP nonces are now stored separately per session and per service, not just one per service
|
|
9
|
+
- the `minisky_console` example now supports restoring previous sessions using session ID
|
|
10
|
+
- for security, `Client#handle_callback` and `Minisky::OAuthClient#handle_callback` now require an `expected_session_id:` argument, where you need to pass a session ID previously saved from the output of `authorize` in browser storage or a local variable; token callback is rejected if the passed saved session ID doesn't match the ID of the session looked up using the state token from the callback
|
|
11
|
+
- added `Session#pending?` and `Session#tokens_valid?`
|
|
12
|
+
- removed unused `default_token_lifetime` and `encryption` configuration settings
|
|
13
|
+
- added `client_assertion_lifetime` configuration setting
|
|
14
|
+
- nonce manager now uses the global `dpop_nonce_lifetime` ttl setting by default
|
|
15
|
+
- simplified the DPoP public key JWT included in headers
|
|
16
|
+
- improved validation of nonce-related headers & responses, client metadata, refresh errors, and assertion signing keys
|
|
17
|
+
- sessions store the client assertion signing key's ID in the metadata and check it when deserialized
|
|
18
|
+
- various refactors and reorganizations
|
|
19
|
+
|
|
1
20
|
## [0.3.0.beta2] – 2026-10-31 👻
|
|
2
21
|
|
|
3
22
|
- private keys for signing are now passed separately as `signing_key:` instead of being taken from the same `jwks` in the metadata that holds public keys
|
data/README.md
CHANGED
|
@@ -62,10 +62,12 @@ AuthProto.configure do |config|
|
|
|
62
62
|
verify_ssl: true
|
|
63
63
|
)
|
|
64
64
|
|
|
65
|
-
# Set
|
|
66
|
-
config.default_token_lifetime = 300 # 5 minutes
|
|
65
|
+
# Set the default nonce cache lifetime (seconds), used by newly created clients.
|
|
67
66
|
config.dpop_nonce_lifetime = 300 # 5 minutes
|
|
68
67
|
|
|
68
|
+
# Set client assertion validity (seconds), used for each newly generated JWT.
|
|
69
|
+
config.client_assertion_lifetime = 300 # 5 minutes
|
|
70
|
+
|
|
69
71
|
# Configure storage backend (default is in-memory)
|
|
70
72
|
config.storage = AuthProto::Storage::Memory.new
|
|
71
73
|
end
|
|
@@ -75,6 +77,16 @@ gem 'sqlite3', '~> 2.9'
|
|
|
75
77
|
gem 'redis', '~> 5.4'
|
|
76
78
|
```
|
|
77
79
|
|
|
80
|
+
Set `dpop_nonce_lifetime` to a positive integer before creating clients or nonce
|
|
81
|
+
managers. Its default is 300 seconds. An explicit `NonceManager.new(ttl: ...)` or
|
|
82
|
+
`DPoP::Client.new(nonce_ttl: ...)` overrides it. Existing managers retain the
|
|
83
|
+
lifetime chosen at initialization.
|
|
84
|
+
|
|
85
|
+
`client_assertion_lifetime` must also be a positive integer and defaults to 300
|
|
86
|
+
seconds. It is read for each assertion, including by existing clients and signers.
|
|
87
|
+
An explicit `ClientAssertion#generate_jwt(lifetime: ...)` overrides the configured
|
|
88
|
+
default. This controls assertion validity, not access-token or session lifetime.
|
|
89
|
+
|
|
78
90
|
### Local HTTP debugging
|
|
79
91
|
|
|
80
92
|
For debugging a flow that needs both local HTTP and remote HTTPS (handle/PLC
|
|
@@ -114,6 +126,19 @@ validated; the original client ID is preserved exactly.
|
|
|
114
126
|
|
|
115
127
|
The library supports multiple storage backends for managing OAuth state:
|
|
116
128
|
|
|
129
|
+
Pending authorization sessions and their state-token lookups expire after 15 minutes by default.
|
|
130
|
+
Configure the lifetime in seconds with a positive integer:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
AuthProto.configure do |config|
|
|
134
|
+
config.pending_session_ttl = 900
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Updating a pending session preserves its original deadline. Successful authorization removes
|
|
139
|
+
the pending deadline and state-token lookup. Authenticated sessions without refresh tokens
|
|
140
|
+
expire with their access token; sessions with refresh tokens have no automatic storage expiry.
|
|
141
|
+
|
|
117
142
|
#### In-Memory Storage (Default)
|
|
118
143
|
|
|
119
144
|
```ruby
|
|
@@ -193,6 +218,42 @@ class CustomStorage < AuthProto::Storage::Interface
|
|
|
193
218
|
end
|
|
194
219
|
```
|
|
195
220
|
|
|
221
|
+
### DPoP Session Keys
|
|
222
|
+
|
|
223
|
+
Each new OAuth session generates its own DPoP key before PAR. The session serializer
|
|
224
|
+
stores its private JWK with the session and encrypts the private `d` component.
|
|
225
|
+
Restoring the session restores that same key for code exchange, refresh and API
|
|
226
|
+
requests. Configure persistent storage and a stable `ATPROTO_MASTER_KEY` to retain
|
|
227
|
+
sessions across application restarts.
|
|
228
|
+
|
|
229
|
+
`Client` and the Minisky integration no longer accept `dpop_key:`. Restore sessions
|
|
230
|
+
by their session ID; no separate key export or constructor argument is needed.
|
|
231
|
+
`Client#dpop_client` remains shared and accepts `session:` when generating proofs.
|
|
232
|
+
Its `ProofGenerator` accepts `key_set:` per call. Nonces are stored separately with
|
|
233
|
+
TTLs, indexed by session ID and server origin. URLs on the same origin share a
|
|
234
|
+
nonce within that session; other sessions and origins have independent nonces.
|
|
235
|
+
`DPoP::Client#with_nonce_handling(session, url) { ... }` runs a request block,
|
|
236
|
+
processes its response, and runs it once more if the server sends a nonce challenge.
|
|
237
|
+
The block must generate a fresh proof and send the request on each invocation.
|
|
238
|
+
The helper returns the final response; other errors and exceptions are left to
|
|
239
|
+
its caller. PAR, code exchange, and refresh use this helper.
|
|
240
|
+
|
|
241
|
+
`DPoP::Client#process_response` accepts the whole HTTP response hash and requires
|
|
242
|
+
`server_url:` and `session_id:` to update the requesting
|
|
243
|
+
session's nonce. When sending requests without `with_nonce_handling`, call it
|
|
244
|
+
for every DPoP response, including successes, failures, and retry responses; missing or empty `DPoP-Nonce` headers raise an error. Nonces must use RFC 9449's
|
|
245
|
+
`NQCHAR` syntax; malformed values and duplicate nonce headers are rejected.
|
|
246
|
+
`has_nonce_error?` recognizes HTTP 400 JSON nonce errors and HTTP 401 DPoP
|
|
247
|
+
authentication challenges. It does not update the nonce or send a retry.
|
|
248
|
+
Direct nonce manager calls to `get`, `update`, and `clear` also
|
|
249
|
+
require `server_url:` and `session_id:` keywords. Old per-server cache entries are
|
|
250
|
+
ignored and expire normally; a missing nonce can be obtained through a server challenge.
|
|
251
|
+
|
|
252
|
+
Serialized sessions from older versions that lack a private DPoP key cannot be
|
|
253
|
+
restored: start authorization again. Loading never generates a replacement key for
|
|
254
|
+
an existing session. Client-authentication signing keys remain separate from these
|
|
255
|
+
per-session DPoP keys.
|
|
256
|
+
|
|
196
257
|
### Confidential Client Example
|
|
197
258
|
|
|
198
259
|
Here's a basic example of using the library in a confidential client application:
|
|
@@ -202,6 +263,7 @@ separately from metadata. It must have a nonempty `kid` and permit ES256 signing
|
|
|
202
263
|
Metadata's `jwks` contains only public keys; the client checks that the matching
|
|
203
264
|
public key and `kid` are advertised and active. A public key may declare
|
|
204
265
|
`use: "sig"` or `key_ops: ["verify"]`.
|
|
266
|
+
Each key in an inline `jwks` must have a distinct, case-sensitive `kid`.
|
|
205
267
|
Other advertised public keys may serve other purposes, such as encryption;
|
|
206
268
|
the matching authentication key must permit signature verification.
|
|
207
269
|
|
|
@@ -209,6 +271,21 @@ With `jwks_uri`, the client still uses your separate private signing key. It doe
|
|
|
209
271
|
not fetch that URL or check the advertised key locally; the authorization server
|
|
210
272
|
fetches it to verify assertions.
|
|
211
273
|
|
|
274
|
+
New confidential-client sessions record the signing key's `kid` as `signing_key_id`
|
|
275
|
+
and its RFC 7638 SHA-256 thumbprint as `signing_key_thumbprint` before PAR. Both
|
|
276
|
+
values persist through callbacks and refreshes without including the private
|
|
277
|
+
client-authentication key. The session manager returns `nil` if the key ID is
|
|
278
|
+
unsupported or its thumbprint no longer matches, leaving stored data intact.
|
|
279
|
+
`Client` supplies a key-ID-to-thumbprint mapping derived from its configured
|
|
280
|
+
private signing key, rather than the public keys advertised in metadata.
|
|
281
|
+
Sessions with a recorded key ID but no thumbprint must authorize again.
|
|
282
|
+
Clients still accept one `signing_key:`; automatic key selection and rotation
|
|
283
|
+
are not implemented.
|
|
284
|
+
|
|
285
|
+
Public-client sessions have no signing key ID. Older serialized sessions without
|
|
286
|
+
this field remain readable and use the configured signing key without an ID check;
|
|
287
|
+
their original key ID cannot be inferred from the stored session.
|
|
288
|
+
|
|
212
289
|
```ruby
|
|
213
290
|
# Initialize client with metadata
|
|
214
291
|
client = AuthProto::Client.new(
|
|
@@ -226,14 +303,17 @@ auth = client.authorize(
|
|
|
226
303
|
scope: "atproto"
|
|
227
304
|
)
|
|
228
305
|
|
|
229
|
-
#
|
|
306
|
+
# Retain the expected ID in this browser's session before redirecting to auth[:url].
|
|
307
|
+
session[:pending_oauth_session_id] = auth.fetch(:session_id)
|
|
230
308
|
|
|
231
309
|
# Handle callback
|
|
232
310
|
tokens = client.handle_callback(
|
|
233
311
|
code: params[:code],
|
|
234
312
|
state: params[:state],
|
|
235
|
-
iss: params[:iss]
|
|
313
|
+
iss: params[:iss],
|
|
314
|
+
expected_session_id: session[:pending_oauth_session_id]
|
|
236
315
|
)
|
|
316
|
+
session.delete(:pending_oauth_session_id)
|
|
237
317
|
|
|
238
318
|
# Make authenticated requests
|
|
239
319
|
headers = client.auth_headers(
|
|
@@ -267,14 +347,37 @@ auth = client.authorize(
|
|
|
267
347
|
scope: "atproto"
|
|
268
348
|
)
|
|
269
349
|
|
|
350
|
+
# Retain this in the initiating browser session, or locally for a native app.
|
|
351
|
+
session[:pending_oauth_session_id] = auth.fetch(:session_id)
|
|
352
|
+
|
|
270
353
|
# After callback, exchange code for tokens
|
|
271
354
|
tokens = client.handle_callback(
|
|
272
355
|
code: params[:code],
|
|
273
356
|
state: params[:state],
|
|
274
|
-
iss: params[:iss]
|
|
357
|
+
iss: params[:iss],
|
|
358
|
+
expected_session_id: session[:pending_oauth_session_id]
|
|
275
359
|
)
|
|
360
|
+
session.delete(:pending_oauth_session_id)
|
|
276
361
|
```
|
|
277
362
|
|
|
363
|
+
### Binding Callbacks to the Initiating Login
|
|
364
|
+
|
|
365
|
+
`handle_callback` requires `expected_session_id:` for both `AuthProto::Client` and
|
|
366
|
+
`Minisky::OAuthClient`. Retain the `:session_id` returned by `authorize` in the
|
|
367
|
+
initiating browser's session, then supply that retained ID on callback. Missing,
|
|
368
|
+
`nil`, empty, or mismatched IDs are rejected before any token exchange.
|
|
369
|
+
|
|
370
|
+
Never take the expected ID from callback parameters or derive it from the returned
|
|
371
|
+
`state`: that would not establish which browser started the login. A shared web
|
|
372
|
+
client's `session_id` is not a substitute for per-browser session storage. Remove
|
|
373
|
+
the pending association after successful completion. For concurrent login attempts,
|
|
374
|
+
retain each expected ID separately and verify membership in that browser's pending attempts.
|
|
375
|
+
|
|
376
|
+
Native and console apps can retain the ID in the local context for that login
|
|
377
|
+
attempt, as the localhost example does. The console example's manual callback helper
|
|
378
|
+
uses the ID remembered by its local Minisky client. When upgrading existing callers,
|
|
379
|
+
add this required keyword using the retained authorization result.
|
|
380
|
+
|
|
278
381
|
## Features In Detail
|
|
279
382
|
|
|
280
383
|
### Identity Resolution
|
|
@@ -168,6 +168,16 @@ export SESSION_SECRET=your-secure-random-string
|
|
|
168
168
|
|
|
169
169
|
If not set, a random secret will be generated on startup (not recommended for production).
|
|
170
170
|
|
|
171
|
+
The app stores the ID returned by `authorize` as `session[:pending_oauth_session_id]`
|
|
172
|
+
before redirecting. On callback, it passes that browser-session value as the required
|
|
173
|
+
`expected_session_id:` argument to `handle_callback`. The library rejects a missing
|
|
174
|
+
or mismatched ID before exchanging the code. After success, the app removes the
|
|
175
|
+
pending association and remembers the authenticated session separately.
|
|
176
|
+
|
|
177
|
+
The expected ID must come from the initiating browser session, never from the
|
|
178
|
+
callback URL or by looking up the returned `state`. This example supports one pending
|
|
179
|
+
login per browser; starting another replaces the previous pending association.
|
|
180
|
+
|
|
171
181
|
## Running the Application
|
|
172
182
|
|
|
173
183
|
### Development
|
|
@@ -34,7 +34,6 @@ class ExampleApp < Sinatra::Base
|
|
|
34
34
|
# Configure AuthProto settings
|
|
35
35
|
AuthProto.configure do |config|
|
|
36
36
|
config.http_client = AuthProto::HTTPClient.new(timeout: 10, verify_ssl: true)
|
|
37
|
-
config.default_token_lifetime = 300
|
|
38
37
|
config.dpop_nonce_lifetime = 300
|
|
39
38
|
|
|
40
39
|
case ENV.fetch("STORAGE_BACKEND", "memory")
|
|
@@ -73,8 +72,7 @@ class ExampleApp < Sinatra::Base
|
|
|
73
72
|
client_id: client_id,
|
|
74
73
|
redirect_uri: metadata['redirect_uris'][0],
|
|
75
74
|
metadata: metadata,
|
|
76
|
-
signing_key: signing_key
|
|
77
|
-
dpop_key: signing_key
|
|
75
|
+
signing_key: signing_key
|
|
78
76
|
)
|
|
79
77
|
end
|
|
80
78
|
|
|
@@ -137,8 +135,8 @@ class ExampleApp < Sinatra::Base
|
|
|
137
135
|
scope: "atproto repo:app.bsky.feed.post"
|
|
138
136
|
)
|
|
139
137
|
|
|
140
|
-
#
|
|
141
|
-
session[:
|
|
138
|
+
# Bind the pending authorization attempt to this browser, separately from any active login.
|
|
139
|
+
session[:pending_oauth_session_id] = auth[:session_id]
|
|
142
140
|
|
|
143
141
|
# Redirect to authorization URL
|
|
144
142
|
redirect auth[:url]
|
|
@@ -154,10 +152,13 @@ class ExampleApp < Sinatra::Base
|
|
|
154
152
|
result = settings.oauth_client.handle_callback(
|
|
155
153
|
code: params[:code],
|
|
156
154
|
state: params[:state],
|
|
157
|
-
iss: params[:iss]
|
|
155
|
+
iss: params[:iss],
|
|
156
|
+
expected_session_id: session[:pending_oauth_session_id]
|
|
158
157
|
)
|
|
159
158
|
|
|
160
|
-
|
|
159
|
+
session.delete(:pending_oauth_session_id)
|
|
160
|
+
|
|
161
|
+
# Remember the authenticated session ID.
|
|
161
162
|
session[:oauth_session_id] = result[:session_id]
|
|
162
163
|
|
|
163
164
|
redirect "/authorized"
|
|
@@ -6,7 +6,7 @@ require 'json'
|
|
|
6
6
|
key = JOSE::JWK.generate_key([:ec, "P-256"])
|
|
7
7
|
|
|
8
8
|
private_jwk = key.to_map.to_h.merge("kid" => "key-1", "use" => "sig")
|
|
9
|
-
public_jwk = key.
|
|
9
|
+
public_jwk = key.to_public_map.to_h.merge("kid" => "key-1", "use" => "sig")
|
|
10
10
|
|
|
11
11
|
config_dir = File.expand_path('../config', __dir__)
|
|
12
12
|
private_path = File.join(config_dir, 'keys.json')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ../..
|
|
3
3
|
specs:
|
|
4
|
-
authproto (0.3.0.
|
|
4
|
+
authproto (0.3.0.beta2)
|
|
5
5
|
didkit (~> 0.5.0)
|
|
6
6
|
jose (~> 1.2)
|
|
7
7
|
jwt (~> 3.3)
|
|
@@ -91,7 +91,7 @@ DEPENDENCIES
|
|
|
91
91
|
sqlite3
|
|
92
92
|
|
|
93
93
|
CHECKSUMS
|
|
94
|
-
authproto (0.3.0.
|
|
94
|
+
authproto (0.3.0.beta2)
|
|
95
95
|
base58 (0.2.3) sha256=fda5c5d3bda8b429ad0d1a5309c2d1e488ba46f0a2ab99a639890f26c61bf3be
|
|
96
96
|
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
97
97
|
bundler (4.0.23) sha256=407ba8a1d928f412847f819aa5f8b6aef7c59fb51feb517112af8a165487287b
|
|
@@ -62,12 +62,19 @@ Once the browser redirects to the callback URL after your approval, the console
|
|
|
62
62
|
|
|
63
63
|
```
|
|
64
64
|
Authentication complete ✓
|
|
65
|
+
Your session id: ......
|
|
65
66
|
```
|
|
66
67
|
|
|
68
|
+
The callback block supplies `expected_session_id:` from the authorization result
|
|
69
|
+
retained locally before opening the browser. The library checks it before exchanging the code.
|
|
70
|
+
|
|
67
71
|
Then, you can run `client.get_request(...)` and `client.post_request(...)` to perform authorized actions on the given account. By default, the example asks for permissions `repo:app.bsky.feed.post` and `rpc:app.bsky.feed.getTimeline?aud=*`, which allow calling `getTimeline` and creating/modifying posts.
|
|
68
72
|
|
|
69
|
-
|
|
73
|
+
To restore a session after closing and reopening the console, write down the sesion ID printed after successful authentication, and then pass it back to the `OAuthClient` constructor:
|
|
70
74
|
|
|
75
|
+
```rb
|
|
76
|
+
client = create_client('your.pds.hostname.here', session_id: '123456-789-abc-...')
|
|
77
|
+
```
|
|
71
78
|
|
|
72
79
|
## Production use
|
|
73
80
|
|
|
@@ -103,10 +110,22 @@ Copy the printed command back to the console:
|
|
|
103
110
|
handle_callback(client, { ... })
|
|
104
111
|
```
|
|
105
112
|
|
|
113
|
+
Use the same local `client` that started this authorization attempt. The helper
|
|
114
|
+
passes its remembered session ID as `expected_session_id:` and forwards only the
|
|
115
|
+
`code`, `state`, and `iss` callback parameters. The expected ID is not supplied by
|
|
116
|
+
the callback page. Each console client supports one current login attempt.
|
|
117
|
+
|
|
106
118
|
This will print:
|
|
107
119
|
|
|
108
120
|
```
|
|
109
121
|
Authentication complete ✓
|
|
122
|
+
Your session id: ......
|
|
110
123
|
```
|
|
111
124
|
|
|
112
125
|
Then, you can run `client.get_request(...)` and `client.post_request(...)` to perform authorized actions on the given account.
|
|
126
|
+
|
|
127
|
+
Again, to restore a session after closing and reopening the console, write down the sesion ID printed after successful authentication, and then pass it back to the `OAuthClient` constructor:
|
|
128
|
+
|
|
129
|
+
```rb
|
|
130
|
+
client = create_client('your.server.hostname', 'your.pds.hostname', session_id: '123456-789-abc-...')
|
|
131
|
+
```
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'authproto/insecure_http_client'
|
|
2
4
|
require 'authproto/localhost_server'
|
|
3
5
|
require 'authproto/minisky'
|
|
@@ -16,8 +18,8 @@ REDIRECT_URI = 'http://127.0.0.1:9292/callback'
|
|
|
16
18
|
SCOPE = 'atproto repo:app.bsky.feed.post rpc:app.bsky.feed.getTimeline?aud=*'
|
|
17
19
|
CLIENT_ID = 'http://localhost/?' + URI.encode_www_form({ redirect_uri: REDIRECT_URI, scope: SCOPE })
|
|
18
20
|
|
|
19
|
-
def create_client(pds)
|
|
20
|
-
Minisky::OAuthClient.new(pds, client_id: CLIENT_ID, redirect_uri: REDIRECT_URI)
|
|
21
|
+
def create_client(pds, session_id: nil)
|
|
22
|
+
Minisky::OAuthClient.new(pds, client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, session_id: session_id)
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def authorize(client)
|
|
@@ -28,7 +30,7 @@ def authorize(client)
|
|
|
28
30
|
puts
|
|
29
31
|
|
|
30
32
|
AuthProto::LocalhostServer.start(client.oauth_client.client_metadata) do |json|
|
|
31
|
-
client.handle_callback(**json)
|
|
33
|
+
client.handle_callback(**json, expected_session_id: auth.fetch(:session_id))
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
puts "Authentication complete ✓"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'authproto/minisky'
|
|
2
4
|
require 'dotenv/load'
|
|
3
5
|
require 'uri'
|
|
@@ -10,10 +12,11 @@ AuthProto.configure do |config|
|
|
|
10
12
|
)
|
|
11
13
|
end
|
|
12
14
|
|
|
13
|
-
def create_client(server_hostname, pds)
|
|
15
|
+
def create_client(server_hostname, pds, session_id: nil)
|
|
14
16
|
Minisky::OAuthClient.new(pds,
|
|
15
17
|
client_id: "https://#{server_hostname}/oauth-client-metadata.json",
|
|
16
|
-
redirect_uri: "https://#{server_hostname}/callback"
|
|
18
|
+
redirect_uri: "https://#{server_hostname}/callback",
|
|
19
|
+
session_id: session_id
|
|
17
20
|
)
|
|
18
21
|
end
|
|
19
22
|
|
|
@@ -25,7 +28,10 @@ def authorize(client)
|
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
def handle_callback(client, json)
|
|
28
|
-
|
|
31
|
+
# Use the ID retained by this local client, not a value copied from the callback page.
|
|
32
|
+
result = client.handle_callback(
|
|
33
|
+
**json.slice(:code, :state, :iss), expected_session_id: client.session_id
|
|
34
|
+
)
|
|
29
35
|
|
|
30
36
|
puts "Authentication complete ✓"
|
|
31
37
|
puts "Your session id: #{result[:session_id]}"
|