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 +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +33 -19
- data/lib/omniauth/line_oidc/version.rb +1 -1
- data/lib/omniauth/strategies/line.rb +45 -22
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 14c42cbc3567835fdb73ea96e8cbb4eedc9018e1a672a241b0783f6822291e13
|
|
4
|
+
data.tar.gz: 54c822e3f40c853af72d947d1b3af03fc08508499dc8461343b6722547916126
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
-
|
|
28
|
-
|
|
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` | `
|
|
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
|
|
105
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
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
|
|
148
|
+
find it:
|
|
135
149
|
|
|
136
|
-
-
|
|
137
|
-
|
|
138
|
-
- The ID token is
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
38
|
-
#
|
|
39
|
-
|
|
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] =
|
|
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
|
-
#
|
|
204
|
-
#
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
222
|
-
|
|
223
|
-
|
|
244
|
+
def stored
|
|
245
|
+
value = session[SESSION_KEY]
|
|
246
|
+
value.is_a?(Hash) ? value : {}
|
|
224
247
|
end
|
|
225
248
|
end
|
|
226
249
|
end
|