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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/README.md +108 -5
  4. data/examples/confidential_client/README.md +10 -0
  5. data/examples/confidential_client/app.rb +8 -7
  6. data/examples/confidential_client/scripts/generate_keys.rb +1 -1
  7. data/examples/minisky_console/Gemfile.lock +2 -2
  8. data/examples/minisky_console/README.md +20 -1
  9. data/examples/minisky_console/localhost.rb +5 -3
  10. data/examples/minisky_console/production.rb +9 -3
  11. data/lib/authproto/client.rb +73 -90
  12. data/lib/authproto/configuration.rb +37 -16
  13. data/lib/authproto/dpop/client.rb +105 -40
  14. data/lib/authproto/dpop/{key_manager.rb → key_set.rb} +54 -120
  15. data/lib/authproto/dpop/nonce_manager.rb +33 -20
  16. data/lib/authproto/dpop/proof_generator.rb +17 -32
  17. data/lib/authproto/dpop.rb +19 -0
  18. data/lib/authproto/http_helpers.rb +85 -0
  19. data/lib/authproto/metadata/authorization_server.rb +12 -6
  20. data/lib/authproto/metadata/client_metadata.rb +18 -5
  21. data/lib/authproto/metadata/resource_server.rb +4 -2
  22. data/lib/authproto/minisky.rb +36 -34
  23. data/lib/authproto/par/client.rb +22 -73
  24. data/lib/authproto/par/client_assertion.rb +47 -16
  25. data/lib/authproto/par/request.rb +4 -8
  26. data/lib/authproto/par.rb +2 -1
  27. data/lib/authproto/serialization/session.rb +31 -13
  28. data/lib/authproto/serialization/token_set.rb +1 -2
  29. data/lib/authproto/state/session.rb +66 -6
  30. data/lib/authproto/state/session_manager.rb +95 -21
  31. data/lib/authproto/state/token_set.rb +16 -16
  32. data/lib/authproto/state.rb +1 -1
  33. data/lib/authproto/storage/key_builder.rb +3 -11
  34. data/lib/authproto/storage/memory.rb +8 -2
  35. data/lib/authproto/token/refresh.rb +26 -59
  36. data/lib/authproto/validated_uri.rb +23 -0
  37. data/lib/authproto/version.rb +1 -1
  38. data/lib/authproto.rb +0 -1
  39. data/sig/authproto/client.rbs +5 -7
  40. data/sig/authproto/dependencies.rbs +2 -0
  41. data/sig/authproto/dpop/client.rbs +11 -4
  42. data/sig/authproto/dpop/{key_manager.rbs → key_set.rbs} +9 -11
  43. data/sig/authproto/dpop/nonce_manager.rbs +4 -4
  44. data/sig/authproto/dpop/proof_generator.rbs +5 -10
  45. data/sig/authproto/dpop.rbs +6 -0
  46. data/sig/authproto/http_helpers.rbs +7 -0
  47. data/sig/authproto/localhost_server.rbs +1 -1
  48. data/sig/authproto/metadata/authorization_server.rbs +1 -0
  49. data/sig/authproto/metadata/client_metadata.rbs +2 -1
  50. data/sig/authproto/metadata/resource_server.rbs +1 -0
  51. data/sig/authproto/minisky_oauth.rbs +5 -2
  52. data/sig/authproto/par/client.rbs +4 -8
  53. data/sig/authproto/par/client_assertion.rbs +4 -0
  54. data/sig/authproto/par/request.rbs +0 -9
  55. data/sig/authproto/state/session.rbs +10 -3
  56. data/sig/authproto/state/session_manager.rbs +8 -3
  57. data/sig/authproto/state/token_set.rbs +1 -2
  58. data/sig/authproto/storage/key_builder.rbs +1 -2
  59. data/sig/authproto/token/refresh.rbs +3 -4
  60. data/sig/authproto.rbs +5 -2
  61. metadata +14 -19
  62. data/.rspec +0 -1
  63. data/.rubocop.yml +0 -31
  64. data/.tm_properties +0 -1
  65. data/.yardopts +0 -5
  66. data/AGENTS.md +0 -80
  67. data/Rakefile +0 -8
  68. data/examples/confidential_client/.gitignore +0 -6
  69. data/examples/minisky_console/.gitignore +0 -2
  70. data/lib/authproto/serialization/dpop_key.rb +0 -38
  71. data/misc/yard_attributes.rb +0 -34
  72. data/sig/authproto/serialization/dpop_key.rbs +0 -14
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a24a4c3d46d68006268cf34364953c33cd23225f2203f2054e7167f8f1470970
4
- data.tar.gz: 80287bbe86fc764f9c8b272d090b8736a17ab76470b5ac0fa32903b94dc64cce
3
+ metadata.gz: 61754b5e7342299c331cabc76c9585ef1f7b1398d0eae4eed252f5d9a2a019bd
4
+ data.tar.gz: 5879610764cb56945d3fc260ed0538f5cdaaabfaec4982af38408290a34a547f
5
5
  SHA512:
6
- metadata.gz: cb4ddce58f52416d16f92dff7f6caf0212c5770addb626610c9459d90894e1c9da4cc664122d359f3a4cdfe572000841cdd3b9daf1ac3defbf472cb3b7fd2474
7
- data.tar.gz: 3dd080bf47dc27d1b318784aad020263ab362d2e49f4fbfd4e2716029841de99f0eb7199ac5ba1483208fe1993967b574793ad01fdd41240fd2e5b37d56ddc7f
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 token lifetimes
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
- # Use auth[:url] to redirect user
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
- # Store session ID in user's browser session
141
- session[:oauth_session_id] = auth[:session_id]
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
- # Store tokens
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.to_public.to_map.to_h.merge("kid" => "key-1", "use" => "sig")
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.beta1)
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.beta1)
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
- (Note, the session is currently lost when you close the console.)
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
- result = client.handle_callback(**json)
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]}"