omniauth-line-oidc 1.0.0 → 1.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f26151039d5a4335bc74e131925ce65064ab85e8d82597d1944bad71c22b283
4
- data.tar.gz: bb091bb1368f5efbfceb229914c646b89a2945cb7441f7be00c7815c1efa1490
3
+ metadata.gz: 14c42cbc3567835fdb73ea96e8cbb4eedc9018e1a672a241b0783f6822291e13
4
+ data.tar.gz: 54c822e3f40c853af72d947d1b3af03fc08508499dc8461343b6722547916126
5
5
  SHA512:
6
- metadata.gz: 3e63eee133d0243a9369dc39883a7ea09e8aa6eccaac857c03a4c15215bb9863c1ab62f08e0e72f3de60098969ae79f329409c7996d33771a953ed84fa2b24be
7
- data.tar.gz: bdb9da23b3657c86127991e2e81acd67a301bd006aa2bbda12e2639937a216ede80919351afe3ef3e1f0c5dd305fafc0b8af339b8c47770e2efd76267d06a5a2
6
+ metadata.gz: bf59ff6faa46a241b517ba0ef8dd3c2be2c1bb3fdb98502e80935fdc8de8c065ff972fc5722573ac95f1b589aaadeff638306a7550f0d5b3ecc68fb79ee41493
7
+ data.tar.gz: cdf79415a87c1121b56a8486efeea7823d69002351615224c7d9ac899e21e8e60fa0832e817bdcb17f3d1a0291d2035db8c5765dcd53ad707d4eb79f4472f8dc
data/CHANGELOG.md CHANGED
@@ -6,6 +6,25 @@ All notable changes to this gem are recorded here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.1.0] - 2026-11-12
10
+
11
+ 1.0.0 could overflow a 4 KB cookie session: with its defaults, five
12
+ unfinished sign-ins raised `ActionDispatch::Cookies::CookieOverflow` in a
13
+ Rails app.
14
+
15
+ ### Changed
16
+
17
+ - PKCE is off unless `pkce: true` is set. `omniauth-oauth2` keeps a
18
+ 128-character verifier in the session for each of the last five
19
+ sign-ins, more than a cookie session has room for. Turn it on when
20
+ sessions are stored on the server.
21
+ - The nonce no longer takes session space per sign-in. The session holds
22
+ one secret and an eight-character fingerprint of each sign-in under
23
+ way, about 110 bytes in all; a sign-in's nonce is derived from the
24
+ secret and its `state`. The session key is `omniauth.line`
25
+ (`omniauth.line.nonces` in 1.0.0). A sign-in in flight across the
26
+ upgrade is verified without a nonce.
27
+
9
28
  ## [1.0.0] - 2026-11-12
10
29
 
11
30
  First release under the name `omniauth-line-oidc`. The gem continues the
data/README.md CHANGED
@@ -24,12 +24,8 @@ Replace the gem in your Gemfile; the provider name, the callback path
24
24
  (`/auth/line/callback`) and the auth hash keys are the same. Things to
25
25
  check:
26
26
 
27
- - PKCE is on by default. It needs nothing from you unless your callback is
28
- handled without the session that started the sign-in; see
29
- [Callbacks handled by another session](#callbacks-handled-by-another-session).
30
- - A sign-in started before the deploy and finished after it has no PKCE
31
- verifier or nonce in its session, and may be refused by LINE; the user
32
- signs in again.
27
+ - A sign-in started before the deploy and finished after it has no nonce
28
+ in its session; its ID token is verified without one.
33
29
  - A sign-in now fails when LINE rejects the ID token. `omniauth-line`
34
30
  ignored the rejection and returned no email.
35
31
  - `extra` now also carries `raw_info`, and `id_token_claims` when the ID
@@ -87,7 +83,7 @@ provider :line, ENV['LINE_CHANNEL_ID'], ENV['LINE_CHANNEL_SECRET'],
87
83
  | Option | Default | Meaning |
88
84
  |---|---|---|
89
85
  | `scope` | `'profile openid'` | Add `email` once the channel has the permission |
90
- | `pkce` | `true` | Send a PKCE challenge (`S256`, the only method LINE accepts) |
86
+ | `pkce` | `false` | Send a PKCE challenge (`S256`, the only method LINE accepts). See [Session size and PKCE](#session-size-and-pkce) before turning it on |
91
87
  | `redirect_uri` | built from the request | The callback URL registered with LINE. `callback_url`, the name `omniauth-line` used, is accepted too |
92
88
  | `verify_options` | `{ open_timeout: 5, read_timeout: 10 }` | Timeouts for the ID token verify call, in seconds |
93
89
 
@@ -101,8 +97,8 @@ token the sign-in was just issued. The ID token adds what the profile
101
97
  API does not have, the email above all.
102
98
 
103
99
  - The strategy sends a `nonce` with every authorization request that
104
- asks for an ID token, and keeps it in the session under the request's
105
- `state`.
100
+ asks for an ID token. The nonce is derived from the request's `state`
101
+ and a secret kept in the session, so each sign-in has its own.
106
102
  - On the callback it sends the ID token, the channel ID and that nonce
107
103
  to LINE's [verify endpoint](https://developers.line.biz/en/reference/line-login/#verify-id-token).
108
104
  LINE checks the signature, issuer, audience, expiry and nonce.
@@ -120,22 +116,40 @@ The raw `extra[:id_token]` is passed through as LINE issued it. Do not
120
116
  read claims out of it yourself; use `extra[:id_token_claims]`, which is
121
117
  only present after verification.
122
118
 
123
- The strategy keeps one nonce for each sign-in a browser session has
124
- under way, up to five, next to what `omniauth-oauth2` stores for state
125
- and PKCE. Five unfinished sign-ins come to about 2.5 KB of session data,
126
- which matters if your sessions live in a 4 KB cookie that is already
127
- well filled.
119
+ ### Session size and PKCE
120
+
121
+ Sessions are often a 4 KB cookie (Rails' default), and a user who starts
122
+ a sign-in and never returns leaves what it stored behind, up to the five
123
+ most recent.
124
+
125
+ - For the nonce the strategy keeps one secret and an eight-character
126
+ fingerprint per sign-in under way: about 110 bytes of session data in
127
+ all, however many there are.
128
+ - `omniauth-oauth2` keeps the state and redirect URI of each sign-in,
129
+ and `omniauth` its origin and params. Set `origin_param: false` on the
130
+ provider unless you read `omniauth.origin`: the origin is the page the
131
+ sign-in started from, stored once per sign-in.
132
+ - With `pkce: true`, `omniauth-oauth2` also keeps a 128-character
133
+ verifier per sign-in. In a Rails app with cookie sessions, five
134
+ unfinished sign-ins with PKCE overflowed the cookie
135
+ (`ActionDispatch::Cookies::CookieOverflow`). That is why PKCE is off
136
+ unless you ask for it; turn it on when your sessions are stored on the
137
+ server.
138
+
139
+ Without PKCE a sign-in is still tied to the session by `state` and to
140
+ the ID token by the nonce, and the token exchange is authenticated with
141
+ the channel secret.
128
142
 
129
143
  ### Callbacks handled by another session
130
144
 
131
- The nonce and the PKCE verifier live in the session that started the
145
+ The nonce's secret, like `state`, lives in the session that started the
132
146
  sign-in. If your callback arrives in a different session (for example a
133
147
  front end on another domain relays it to your API), the strategy cannot
134
- find them:
148
+ find it:
135
149
 
136
- - Set `pkce: false`, or the token exchange is sent without a verifier
137
- and LINE rejects it.
138
- - The ID token is then verified without a nonce. Everything else about
150
+ - Leave `pkce` off: there would be no verifier to send, and LINE would
151
+ reject the token exchange.
152
+ - The ID token is verified without a nonce. Everything else about
139
153
  it is still checked.
140
154
  - You will also need `provider_ignores_state: true`, which gives up the
141
155
  CSRF protection `state` provides. Prefer carrying the session across
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OmniAuth
4
4
  module LineOidc
5
- VERSION = '1.0.0'
5
+ VERSION = '1.1.0'
6
6
  end
7
7
  end
@@ -19,10 +19,10 @@ module OmniAuth
19
19
  # claims.
20
20
  class Line < OmniAuth::Strategies::OAuth2
21
21
  VERIFY_URL = 'https://api.line.me/oauth2/v2.1/verify'
22
- NONCE_SESSION_KEY = 'omniauth.line.nonces'
22
+ SESSION_KEY = 'omniauth.line'
23
23
  # Sign-ins a browser session can have under way at once, each with its
24
24
  # own nonce; the oldest is dropped beyond this.
25
- MAX_STORED_NONCES = 5
25
+ MAX_PENDING_SIGN_INS = 5
26
26
 
27
27
  # Failures that say nothing about the token: LINE was not reached, or
28
28
  # did not answer.
@@ -33,10 +33,11 @@ module OmniAuth
33
33
  option :name, 'line'
34
34
  option :scope, 'profile openid'
35
35
 
36
- # LINE supports PKCE with S256 only, which is what omniauth-oauth2 sends.
37
- # Set `pkce: false` when the callback is handled without the session
38
- # that started the sign-in, since the verifier lives in that session.
39
- option :pkce, true
36
+ # LINE supports PKCE with S256 only, which is what omniauth-oauth2 sends
37
+ # with `pkce: true`. It is off unless asked for because omniauth-oauth2
38
+ # keeps a 128-character verifier in the session for each of the last
39
+ # five sign-ins, which a 4 KB cookie session has no room for.
40
+ option :pkce, false
40
41
 
41
42
  # omniauth-oauth2 only forwards options named here to the authorize
42
43
  # URL; anything else set on the strategy is silently dropped. `nonce` is
@@ -91,8 +92,7 @@ module OmniAuth
91
92
  super.tap do |params|
92
93
  next unless openid_scope?(params[:scope])
93
94
 
94
- params[:nonce] = SecureRandom.hex(16)
95
- remember_nonce(params[:state], params[:nonce])
95
+ params[:nonce] = issue_nonce(params[:state].to_s)
96
96
  end
97
97
  end
98
98
 
@@ -200,27 +200,50 @@ module OmniAuth
200
200
  nil
201
201
  end
202
202
 
203
- # Nonces are kept per `state`, so two sign-ins started in one browser
204
- # session each verify against their own.
205
- def remember_nonce(state, nonce)
206
- nonces = stored_nonces
207
- nonces.delete(state.to_s)
208
- nonces[state.to_s] = nonce.to_s
209
- session[NONCE_SESSION_KEY] = nonces.to_a.last(MAX_STORED_NONCES).to_h
203
+ # The session is often a 4 KB cookie, so it holds one secret and a short
204
+ # fingerprint of each sign-in under way rather than a nonce per sign-in.
205
+ # A sign-in's nonce is derived from the secret and its `state`, which
206
+ # gives two sign-ins started in one browser session each their own.
207
+ def issue_nonce(state)
208
+ secret = stored_secret || SecureRandom.hex(16)
209
+ pending = (stored_pending - [fingerprint(secret, state)]) << fingerprint(secret, state)
210
+ session[SESSION_KEY] = { 'secret' => secret, 'pending' => pending.last(MAX_PENDING_SIGN_INS) }
211
+ nonce_for(secret, state)
210
212
  end
211
213
 
212
214
  # nil when this callback's session did not start the sign-in; the
213
215
  # verify call then checks everything about the token except the nonce.
214
216
  def take_nonce
215
- nonces = stored_nonces
216
- nonce = nonces.delete(request.params['state'].to_s)
217
- session[NONCE_SESSION_KEY] = nonces if nonce
218
- nonce
217
+ secret = stored_secret
218
+ state = request.params['state'].to_s
219
+ pending = stored_pending
220
+ return unless secret && pending.delete(fingerprint(secret, state))
221
+
222
+ session[SESSION_KEY] = { 'secret' => secret, 'pending' => pending }
223
+ nonce_for(secret, state)
224
+ end
225
+
226
+ def nonce_for(secret, state)
227
+ OpenSSL::HMAC.hexdigest('SHA256', secret, "nonce:#{state}")[0, 32]
228
+ end
229
+
230
+ def fingerprint(secret, state)
231
+ OpenSSL::HMAC.hexdigest('SHA256', secret, "state:#{state}")[0, 8]
232
+ end
233
+
234
+ def stored_secret
235
+ secret = stored['secret']
236
+ secret if secret.is_a?(String) && !secret.empty?
237
+ end
238
+
239
+ def stored_pending
240
+ pending = stored['pending']
241
+ pending.is_a?(Array) ? pending.dup : []
219
242
  end
220
243
 
221
- def stored_nonces
222
- nonces = session[NONCE_SESSION_KEY]
223
- nonces.is_a?(Hash) ? nonces.dup : {}
244
+ def stored
245
+ value = session[SESSION_KEY]
246
+ value.is_a?(Hash) ? value : {}
224
247
  end
225
248
  end
226
249
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniauth-line-oidc
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kazasiki