From b48e487cc4df2373fbccbca8d4b79087ceaea9bc Mon Sep 17 00:00:00 2001 From: Lawrence Lin Date: Wed, 2 Sep 2026 21:52:59 -0500 Subject: [PATCH 1/8] fix: forward LINE-specific authorize options omniauth-oauth2 only forwards options named in authorize_options (default scope/state) to the authorize URL, so prompt, bot_prompt, ui_locales and every other LINE parameter set on the strategy was silently dropped. Claude-Session: https://claude.ai/code/session_01RBRHMr4tD5hZ5Js6WQSHr5 --- lib/omniauth/strategies/line.rb | 8 ++++++++ spec/omniauth/strategies/line_spec.rb | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/lib/omniauth/strategies/line.rb b/lib/omniauth/strategies/line.rb index dd07e0c..09ba531 100644 --- a/lib/omniauth/strategies/line.rb +++ b/lib/omniauth/strategies/line.rb @@ -8,6 +8,14 @@ class Line < OmniAuth::Strategies::OAuth2 option :name, 'line' option :scope, 'profile openid' + # omniauth-oauth2 only forwards options named here to the authorize + # URL; anything else set on the strategy is silently dropped. + option :authorize_options, %i[ + scope state prompt nonce max_age ui_locales bot_prompt + initial_amr_display switch_amr disable_auto_login + disable_ios_auto_login + ] + # Authorization lives on access.line.me while every API call # (token exchange, profile, verify) lives on api.line.me. option :client_options, { diff --git a/spec/omniauth/strategies/line_spec.rb b/spec/omniauth/strategies/line_spec.rb index b93f752..1e92dfd 100644 --- a/spec/omniauth/strategies/line_spec.rb +++ b/spec/omniauth/strategies/line_spec.rb @@ -39,6 +39,23 @@ end end + describe 'authorize params' do + it 'should forward LINE-specific options to the authorize request' do + @options = { prompt: 'consent', bot_prompt: 'normal', ui_locales: 'zh-TW' } + allow(subject).to receive(:session).and_return({}) + params = subject.authorize_params + expect(params[:prompt]).to eq('consent') + expect(params[:bot_prompt]).to eq('normal') + expect(params[:ui_locales]).to eq('zh-TW') + end + + it 'should not forward options outside the whitelist' do + @options = { channel_secret: 'super-secret' } + allow(subject).to receive(:session).and_return({}) + expect(subject.authorize_params).not_to have_key(:channel_secret) + end + end + describe 'callback_url' do it 'should exclude the query string' do allow(subject).to receive(:full_host).and_return('https://example.com') From 50b15b576cdd2f9c1c138e82e0648ac92a8f2733 Mon Sep 17 00:00:00 2001 From: Lawrence Lin Date: Wed, 2 Sep 2026 21:53:12 -0500 Subject: [PATCH 2/8] fix: honor an explicitly configured redirect_uri Apps that compute their callback URL (multi-host setups resolving the tenant from the request) set options[:redirect_uri], which the previous callback_url override ignored in favor of the request host. Claude-Session: https://claude.ai/code/session_01RBRHMr4tD5hZ5Js6WQSHr5 --- lib/omniauth/strategies/line.rb | 2 +- spec/omniauth/strategies/line_spec.rb | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/omniauth/strategies/line.rb b/lib/omniauth/strategies/line.rb index 09ba531..e53d62e 100644 --- a/lib/omniauth/strategies/line.rb +++ b/lib/omniauth/strategies/line.rb @@ -69,7 +69,7 @@ def raw_info end def callback_url - full_host + script_name + callback_path + options[:redirect_uri] || (full_host + script_name + callback_path) end end end diff --git a/spec/omniauth/strategies/line_spec.rb b/spec/omniauth/strategies/line_spec.rb index 1e92dfd..3f755de 100644 --- a/spec/omniauth/strategies/line_spec.rb +++ b/spec/omniauth/strategies/line_spec.rb @@ -62,6 +62,11 @@ allow(subject).to receive(:script_name).and_return('') expect(subject.callback_url).to eq('https://example.com/auth/line/callback') end + + it 'should prefer an explicitly configured redirect_uri' do + @options = { redirect_uri: 'https://school.example.com/api/auth/line/callback' } + expect(subject.callback_url).to eq('https://school.example.com/api/auth/line/callback') + end end describe 'callback data' do From 7e6925cc9fe020b6c1407bd3d972b0758993dd0b Mon Sep 17 00:00:00 2001 From: Lawrence Lin Date: Wed, 2 Sep 2026 21:53:43 -0500 Subject: [PATCH 3/8] fix: harden the outbound token and verify calls - Pin the token exchange to client_secret_post: LINE documents only body credentials; the oauth2 gem's basic-auth default happens to work today but is undocumented LINE behavior. - Give the id-token verify call explicit open/read timeouts and degrade email to nil (with a logged warning) on timeout, rejection, or a non-JSON body instead of failing the whole login. - Map OAuth2::TimeoutError from the profile call to ::Timeout::Error so omniauth-oauth2's callback_phase turns it into fail!(:timeout) rather than a 500. Claude-Session: https://claude.ai/code/session_01RBRHMr4tD5hZ5Js6WQSHr5 --- lib/omniauth/strategies/line.rb | 51 ++++++++++++++++++++++----- spec/omniauth/strategies/line_spec.rb | 45 +++++++++++++++++++++-- 2 files changed, 85 insertions(+), 11 deletions(-) diff --git a/lib/omniauth/strategies/line.rb b/lib/omniauth/strategies/line.rb index e53d62e..07a1a2a 100644 --- a/lib/omniauth/strategies/line.rb +++ b/lib/omniauth/strategies/line.rb @@ -16,12 +16,20 @@ class Line < OmniAuth::Strategies::OAuth2 disable_ios_auto_login ] + # The verify call runs inside the user-facing callback request, so a + # slow LINE API must fail fast rather than hold the login hostage. + option :verify_options, { open_timeout: 5, read_timeout: 10 } + # Authorization lives on access.line.me while every API call # (token exchange, profile, verify) lives on api.line.me. + # LINE documents only client_secret_post for the token endpoint; the + # oauth2 gem's basic-auth default happens to work but is undocumented + # LINE behavior. option :client_options, { site: 'https://api.line.me', authorize_url: 'https://access.line.me/oauth2/v2.1/authorize', - token_url: '/oauth2/v2.1/token' + token_url: '/oauth2/v2.1/token', + auth_scheme: :request_body } uid { raw_info['userId'] } @@ -44,17 +52,25 @@ class Line < OmniAuth::Strategies::OAuth2 # LINE returns the email claim only inside the ID token (email scope), # never from the profile endpoint; the verify endpoint decodes it. + # Email is best-effort: any verify failure degrades to nil instead of + # failing the whole login. def email id_token = access_token['id_token'] return nil unless id_token - params = { - id_token: id_token, - client_id: client.id - } - - response = Net::HTTP.post_form(URI('https://api.line.me/oauth2/v2.1/verify'), params) - JSON.parse(response.body)['email'] + response = verify_id_token(id_token) + payload = JSON.parse(response.body) + unless response.is_a?(Net::HTTPSuccess) + log :warn, "ID token verify failed (HTTP #{response.code}): #{payload['error_description'] || payload['error']}" + return nil + end + payload['email'] + rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ETIMEDOUT => e + log :warn, "ID token verify timed out (#{e.class})" + nil + rescue JSON::ParserError + log :warn, 'ID token verify returned a non-JSON body' + nil end # Require: Access token with PROFILE permission issued. @@ -64,13 +80,30 @@ def raw_info profile['email'] = email profile end - rescue ::Errno::ETIMEDOUT + rescue ::OAuth2::TimeoutError, ::Errno::ETIMEDOUT + # omniauth-oauth2's callback_phase only maps ::Timeout::Error and + # ::Errno::ETIMEDOUT to fail!(:timeout); OAuth2::TimeoutError is a + # Faraday subclass it would let escape as a 500. raise ::Timeout::Error end def callback_url options[:redirect_uri] || (full_host + script_name + callback_path) end + + private + + def verify_id_token(id_token) + uri = URI('https://api.line.me/oauth2/v2.1/verify') + request = Net::HTTP::Post.new(uri) + request.set_form_data(id_token: id_token, client_id: client.id) + Net::HTTP.start(uri.host, uri.port, + use_ssl: true, + open_timeout: options.verify_options[:open_timeout], + read_timeout: options.verify_options[:read_timeout]) do |http| + http.request(request) + end + end end end end diff --git a/spec/omniauth/strategies/line_spec.rb b/spec/omniauth/strategies/line_spec.rb index 3f755de..6bca663 100644 --- a/spec/omniauth/strategies/line_spec.rb +++ b/spec/omniauth/strategies/line_spec.rb @@ -37,6 +37,17 @@ it 'should build the token url on api.line.me' do expect(subject.client.token_url).to eq('https://api.line.me/oauth2/v2.1/token') end + + it 'should send client credentials in the token request body' do + stub_request(:post, 'https://api.line.me/oauth2/v2.1/token') + .to_return(:status => 200, :body => { 'access_token' => 't' }.to_json, + :headers => { 'Content-Type' => 'application/json' }) + subject.client.auth_code.get_token('a-code', redirect_uri: 'https://example.com/cb') + expect( + a_request(:post, 'https://api.line.me/oauth2/v2.1/token') + .with(:body => hash_including('client_id' => 'channel_id', 'client_secret' => 'secret')) + ).to have_been_made + end end describe 'authorize params' do @@ -143,12 +154,42 @@ expect(a_request(:get, 'https://api.line.me/v2/profile')).to have_been_made.once end - it 'should convert connection timeouts into Timeout::Error' do - stub_request(:post, 'https://api.line.me/oauth2/v2.1/verify').to_raise(Errno::ETIMEDOUT) + it 'should convert profile connection timeouts into Timeout::Error' do + stub_request(:get, 'https://api.line.me/v2/profile').to_raise(Errno::ETIMEDOUT) expect { subject.raw_info }.to raise_error(Timeout::Error) end end + describe 'email degradation' do + it 'should return no email when the verify call times out' do + stub_request(:post, 'https://api.line.me/oauth2/v2.1/verify').to_timeout + expect(subject.info[:email]).to be_nil + end + + it 'should return no email when the verify call is rejected' do + stub_request(:post, 'https://api.line.me/oauth2/v2.1/verify') + .to_return(:status => 400, + :body => { 'error' => 'invalid_request', 'error_description' => 'Invalid IdToken.' }.to_json, + :headers => { 'Content-Type' => 'application/json' }) + expect(subject.info[:email]).to be_nil + end + + it 'should return no email when the verify response is not JSON' do + stub_request(:post, 'https://api.line.me/oauth2/v2.1/verify') + .to_return(:status => 502, :body => 'Bad Gateway') + expect(subject.info[:email]).to be_nil + end + + it 'should log a warning on verify failure' do + stub_request(:post, 'https://api.line.me/oauth2/v2.1/verify') + .to_return(:status => 400, + :body => { 'error' => 'invalid_request', 'error_description' => 'Invalid IdToken.' }.to_json, + :headers => { 'Content-Type' => 'application/json' }) + expect(subject).to receive(:log).with(:warn, /Invalid IdToken/) + subject.info + end + end + context 'when no id token was issued' do let(:access_token) do OAuth2::AccessToken.new(subject.client, 'access-token', 'scope' => 'profile') From 973059969c075f11dfc617d3ac5297bac0f83e0f Mon Sep 17 00:00:00 2001 From: Lawrence Lin Date: Wed, 2 Sep 2026 21:54:04 -0500 Subject: [PATCH 4/8] test: replay recorded LINE API error responses via VCR Cassettes under spec/cassettes were recorded once against the real LINE API using throwaway credentials (no secrets involved) and are replayed from then on, locking the suite to LINE's actual error shapes for the token, verify, and profile endpoints. record: :once re-records only when a cassette file is deleted; CI never hits the network. Claude-Session: https://claude.ai/code/session_01RBRHMr4tD5hZ5Js6WQSHr5 --- Gemfile | 1 + .../profile_invalid_access_token.yml | 53 ++++++++++++++++++ spec/cassettes/token_invalid_client.yml | 51 ++++++++++++++++++ spec/cassettes/verify_invalid_id_token.yml | 54 +++++++++++++++++++ spec/omniauth/strategies/line_spec.rb | 34 ++++++++++++ spec/spec_helper.rb | 9 ++++ 6 files changed, 202 insertions(+) create mode 100644 spec/cassettes/profile_invalid_access_token.yml create mode 100644 spec/cassettes/token_invalid_client.yml create mode 100644 spec/cassettes/verify_invalid_id_token.yml diff --git a/Gemfile b/Gemfile index 3b0b968..594f7c8 100644 --- a/Gemfile +++ b/Gemfile @@ -8,5 +8,6 @@ group :test do gem 'rspec', '~> 3.2' gem 'rack-test' gem 'simplecov' + gem 'vcr' gem 'webmock' end diff --git a/spec/cassettes/profile_invalid_access_token.yml b/spec/cassettes/profile_invalid_access_token.yml new file mode 100644 index 0000000..fed1509 --- /dev/null +++ b/spec/cassettes/profile_invalid_access_token.yml @@ -0,0 +1,53 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.line.me/v2/profile + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday v2.14.3 + Authorization: + - Bearer invalid-access-token + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 401 + message: Unauthorized + headers: + Content-Type: + - application/json;charset=UTF-8 + Server: + - Moon/0.129.0 + Www-Authenticate: + - Bearer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-Line-Request-Id: + - a515a11a211e45b4 + X-Xss-Protection: + - '0' + Content-Length: + - '27' + Expires: + - Thu, 03 Sep 2026 02:49:01 GMT + Cache-Control: + - max-age=0, no-cache, no-store + Pragma: + - no-cache + Date: + - Thu, 03 Sep 2026 02:49:01 GMT + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"message":"invalid token"}' + recorded_at: Thu, 03 Sep 2026 02:49:01 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/cassettes/token_invalid_client.yml b/spec/cassettes/token_invalid_client.yml new file mode 100644 index 0000000..1507904 --- /dev/null +++ b/spec/cassettes/token_invalid_client.yml @@ -0,0 +1,51 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.line.me/oauth2/v2.1/token + body: + encoding: UTF-8 + string: client_id=channel_id&client_secret=secret&code=bogus-code&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fexample.com%2Fauth%2Fline%2Fcallback + headers: + User-Agent: + - Faraday v2.14.3 + Content-Type: + - application/x-www-form-urlencoded + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 400 + message: Bad Request + headers: + Content-Type: + - application/json;charset=UTF-8 + Server: + - Moon/0.129.0 + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-Line-Request-Id: + - 7c5c77f216ab376f + X-Xss-Protection: + - '0' + Content-Length: + - '72' + Expires: + - Thu, 03 Sep 2026 02:52:14 GMT + Cache-Control: + - max-age=0, no-cache, no-store + Pragma: + - no-cache + Date: + - Thu, 03 Sep 2026 02:52:14 GMT + Connection: + - close + body: + encoding: UTF-8 + string: '{"error":"invalid_request","error_description":"client_id is required."}' + recorded_at: Thu, 03 Sep 2026 02:52:14 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/cassettes/verify_invalid_id_token.yml b/spec/cassettes/verify_invalid_id_token.yml new file mode 100644 index 0000000..ff0d2b6 --- /dev/null +++ b/spec/cassettes/verify_invalid_id_token.yml @@ -0,0 +1,54 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.line.me/oauth2/v2.1/verify + body: + encoding: US-ASCII + string: id_token=bogus.id.token&client_id=channel_id + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Host: + - api.line.me + Content-Type: + - application/x-www-form-urlencoded + response: + status: + code: 400 + message: Bad Request + headers: + Content-Type: + - application/json;charset=UTF-8 + Server: + - Moon/0.129.0 + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-Line-Request-Id: + - 8f3f565f12943167 + X-Xss-Protection: + - '0' + Content-Length: + - '85' + Expires: + - Thu, 03 Sep 2026 02:49:01 GMT + Cache-Control: + - max-age=0, no-cache, no-store + Pragma: + - no-cache + Date: + - Thu, 03 Sep 2026 02:49:01 GMT + Connection: + - close + body: + encoding: UTF-8 + string: '{"error":"invalid_request","error_description":"some parameters in + the wrong format"}' + recorded_at: Thu, 03 Sep 2026 02:49:01 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/omniauth/strategies/line_spec.rb b/spec/omniauth/strategies/line_spec.rb index 6bca663..9878237 100644 --- a/spec/omniauth/strategies/line_spec.rb +++ b/spec/omniauth/strategies/line_spec.rb @@ -222,4 +222,38 @@ end end end + + # Cassettes under spec/cassettes were recorded against the real LINE API + # with throwaway credentials (no secrets involved); CI only replays them. + describe 'recorded LINE API behavior' do + let(:access_token) do + OAuth2::AccessToken.new(subject.client, 'invalid-access-token', + 'id_token' => 'bogus.id.token', 'scope' => 'profile openid email') + end + + before do + allow(subject).to receive(:access_token).and_return(access_token) + end + + it 'should degrade to a nil email when LINE rejects the id token' do + VCR.use_cassette('verify_invalid_id_token') do + expect(subject.email).to be_nil + end + end + + it 'should raise OAuth2::Error when LINE rejects the profile access token' do + VCR.use_cassette('profile_invalid_access_token') do + expect { subject.raw_info }.to raise_error(OAuth2::Error) + end + end + + it 'should raise OAuth2::Error when the token exchange is rejected' do + VCR.use_cassette('token_invalid_client') do + expect { + subject.client.auth_code.get_token('bogus-code', + redirect_uri: 'https://example.com/auth/line/callback') + }.to raise_error(OAuth2::Error) + end + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index bf2ffa6..adc79e3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,9 +7,18 @@ require 'rspec' require 'rack/test' require 'webmock/rspec' +require 'vcr' require 'omniauth' require 'omniauth-line' +VCR.configure do |config| + config.cassette_library_dir = File.expand_path('cassettes', __dir__) + config.hook_into :webmock + # :once records against the real LINE API when a cassette is missing and + # replays it forever after; CI only ever replays committed cassettes. + config.default_cassette_options = { record: :once } +end + RSpec.configure do |config| config.include WebMock::API config.include Rack::Test::Methods From e556a0ff00c015bb828d042309c94b837dccc078 Mon Sep 17 00:00:00 2001 From: Lawrence Lin Date: Wed, 2 Sep 2026 22:15:34 -0500 Subject: [PATCH 5/8] fix: fall back to default verify timeouts on partial overrides Claude-Session: https://claude.ai/code/session_01RBRHMr4tD5hZ5Js6WQSHr5 --- lib/omniauth/strategies/line.rb | 8 +++++--- spec/omniauth/strategies/line_spec.rb | 5 +++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/omniauth/strategies/line.rb b/lib/omniauth/strategies/line.rb index 07a1a2a..c935795 100644 --- a/lib/omniauth/strategies/line.rb +++ b/lib/omniauth/strategies/line.rb @@ -18,7 +18,8 @@ class Line < OmniAuth::Strategies::OAuth2 # The verify call runs inside the user-facing callback request, so a # slow LINE API must fail fast rather than hold the login hostage. - option :verify_options, { open_timeout: 5, read_timeout: 10 } + DEFAULT_VERIFY_TIMEOUTS = { open_timeout: 5, read_timeout: 10 }.freeze + option :verify_options, DEFAULT_VERIFY_TIMEOUTS.dup # Authorization lives on access.line.me while every API call # (token exchange, profile, verify) lives on api.line.me. @@ -97,10 +98,11 @@ def verify_id_token(id_token) uri = URI('https://api.line.me/oauth2/v2.1/verify') request = Net::HTTP::Post.new(uri) request.set_form_data(id_token: id_token, client_id: client.id) + timeouts = options.verify_options || {} Net::HTTP.start(uri.host, uri.port, use_ssl: true, - open_timeout: options.verify_options[:open_timeout], - read_timeout: options.verify_options[:read_timeout]) do |http| + open_timeout: timeouts[:open_timeout] || DEFAULT_VERIFY_TIMEOUTS[:open_timeout], + read_timeout: timeouts[:read_timeout] || DEFAULT_VERIFY_TIMEOUTS[:read_timeout]) do |http| http.request(request) end end diff --git a/spec/omniauth/strategies/line_spec.rb b/spec/omniauth/strategies/line_spec.rb index 9878237..05e6536 100644 --- a/spec/omniauth/strategies/line_spec.rb +++ b/spec/omniauth/strategies/line_spec.rb @@ -180,6 +180,11 @@ expect(subject.info[:email]).to be_nil end + it 'should fall back to default timeouts when verify_options is overridden away' do + @options = { verify_options: nil } + expect(subject.info[:email]).to eq('foo@example.com') + end + it 'should log a warning on verify failure' do stub_request(:post, 'https://api.line.me/oauth2/v2.1/verify') .to_return(:status => 400, From ab405aa08ba8b354f1581b4b3c80facdf30a8ff1 Mon Sep 17 00:00:00 2001 From: Lawrence Lin Date: Wed, 2 Sep 2026 22:15:34 -0500 Subject: [PATCH 6/8] test: redact secrets in cassettes and make CI replay-only filter_sensitive_data scrubs the channel secret from future recordings (the committed cassette is scrubbed retroactively), and record: :none on CI turns a missing cassette into a hard failure instead of a live call. Claude-Session: https://claude.ai/code/session_01RBRHMr4tD5hZ5Js6WQSHr5 --- spec/cassettes/token_invalid_client.yml | 2 +- spec/spec_helper.rb | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/spec/cassettes/token_invalid_client.yml b/spec/cassettes/token_invalid_client.yml index 1507904..0c1d959 100644 --- a/spec/cassettes/token_invalid_client.yml +++ b/spec/cassettes/token_invalid_client.yml @@ -5,7 +5,7 @@ http_interactions: uri: https://api.line.me/oauth2/v2.1/token body: encoding: UTF-8 - string: client_id=channel_id&client_secret=secret&code=bogus-code&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fexample.com%2Fauth%2Fline%2Fcallback + string: client_id=channel_id&client_secret=%3CCHANNEL_SECRET%3E&code=bogus-code&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fexample.com%2Fauth%2Fline%2Fcallback headers: User-Agent: - Faraday v2.14.3 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index adc79e3..3cccb02 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -15,8 +15,10 @@ config.cassette_library_dir = File.expand_path('cassettes', __dir__) config.hook_into :webmock # :once records against the real LINE API when a cassette is missing and - # replays it forever after; CI only ever replays committed cassettes. - config.default_cassette_options = { record: :once } + # replays it forever after; on CI a missing cassette is a hard failure + # instead of a network call. + config.default_cassette_options = { record: ENV['CI'] ? :none : :once } + config.filter_sensitive_data('') { 'secret' } end RSpec.configure do |config| From b99c8d14d971fa1b644ef6aa9b069a7bf73bc90c Mon Sep 17 00:00:00 2001 From: Lawrence Lin Date: Wed, 2 Sep 2026 22:23:23 -0500 Subject: [PATCH 7/8] test: scrub LINE_CHANNEL_SECRET from any future recording Claude-Session: https://claude.ai/code/session_01RBRHMr4tD5hZ5Js6WQSHr5 --- spec/spec_helper.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3cccb02..f3483a1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -19,6 +19,9 @@ # instead of a network call. config.default_cassette_options = { record: ENV['CI'] ? :none : :once } config.filter_sensitive_data('') { 'secret' } + # Anyone re-recording against a real channel exports LINE_CHANNEL_SECRET; + # scrub it so real credentials can never land in committed YAML. + config.filter_sensitive_data('') { ENV['LINE_CHANNEL_SECRET'] } if ENV['LINE_CHANNEL_SECRET'] end RSpec.configure do |config| From 213325900ba4dfa00a7eaa19d64d1f650f27a8d2 Mon Sep 17 00:00:00 2001 From: Vincent Li Date: Tue, 8 Sep 2026 10:11:17 +0800 Subject: [PATCH 8/8] test: stub the recorded-behavior token with LINE's real scope string --- spec/omniauth/strategies/line_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/omniauth/strategies/line_spec.rb b/spec/omniauth/strategies/line_spec.rb index 05e6536..a30aae9 100644 --- a/spec/omniauth/strategies/line_spec.rb +++ b/spec/omniauth/strategies/line_spec.rb @@ -233,7 +233,7 @@ describe 'recorded LINE API behavior' do let(:access_token) do OAuth2::AccessToken.new(subject.client, 'invalid-access-token', - 'id_token' => 'bogus.id.token', 'scope' => 'profile openid email') + 'id_token' => 'bogus.id.token', 'scope' => 'profile openid') end before do