omniauth-yahoojp 1.0.0 → 1.0.1

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: 16ae4d397a52eca91f5a45f36770e3ec1162bda6f607a1c2bbd5142ee9799b9c
4
- data.tar.gz: 7989bc85f42e4ae4e6b2420e5d3a6c94355a87db5ba0997b1a1a8a62249ae088
3
+ metadata.gz: cef0fc73e6ad6338ec0f4a4180774ab663b9a0846ad2e7658f9a4c3fd395b454
4
+ data.tar.gz: 37e8e9ef4282fc449ea7d489ef4a43ea4ab0d231c7ddda5a6163b932c899d7ac
5
5
  SHA512:
6
- metadata.gz: c6f857f7d5604801d3aff235992b49d925a889e6ca6458400ceb746abe617594a8fa5ab229312f332f34df6fae83d3af508b33f39fd5d3c93bf4a611884395d9
7
- data.tar.gz: 8c2a1ca936c8c89adbd4655d01021e14a7e669965be3015fd028df1bcb0c82164a66b90f116056c9d21963f6d5b22ae0ca3329ea9e13001336149f921f344be0
6
+ metadata.gz: cf6a1d9162d5855cd7ed66074b9f5d275312024602fb956a9e367e522aeaf36d3637c73856c3cd94b3dcc5f0d53ea01697aec948370689b766d0b6f6890a29c3
7
+ data.tar.gz: 84aa02e550b3fd6bbc286912be6181b484b2d7d1db6ab6ff8792a9819575b08b0c77e406b56045fdac3073cb4900500eb07a7ab91641de909ffb1881d8953043
data/README.md CHANGED
@@ -65,9 +65,16 @@ Controls how user profile information is retrieved.
65
65
  When the `openid` scope is requested, the strategy automatically captures the `id_token` returned by Yahoo! JAPAN's token endpoint.
66
66
 
67
67
  - `credentials.id_token` — The raw JWT string as returned from the token endpoint.
68
- - `extra.id_token_claims` — The decoded claims hash from the `id_token`.
68
+ - `extra.id_token_claims` — The decoded and verified claims hash from the `id_token`.
69
69
 
70
- The `id_token` signature verification is skipped because the token is received directly from Yahoo! JAPAN's token endpoint over TLS in the Authorization Code Flow, which guarantees its authenticity.
70
+ The `id_token` is verified as follows:
71
+
72
+ 1. **RS256 signature verification** — The token signature is verified using the public key fetched from Yahoo! JAPAN's [JWKS endpoint](https://auth.login.yahoo.co.jp/yconnect/v2/jwks), matched by the `kid` header claim.
73
+ 2. **Issuer (`iss`) validation** — Must be `https://auth.login.yahoo.co.jp/yconnect/v2`.
74
+ 3. **Audience (`aud`) validation** — Must include your application's client ID.
75
+ 4. **Expiration (`exp`) validation** — The token must not be expired (with a 30-second leeway for clock skew).
76
+
77
+ If any verification step fails, an `OmniAuth::Strategies::YahooJp::IdTokenValidationError` is raised, which is handled by OmniAuth's standard error flow.
71
78
 
72
79
  ### API Version
73
80
 
@@ -5,6 +5,10 @@ require 'json/jwt'
5
5
  module OmniAuth
6
6
  module Strategies
7
7
  class YahooJp < OmniAuth::Strategies::OAuth2
8
+ class IdTokenValidationError < StandardError; end
9
+
10
+ JWKS_URI = 'https://auth.login.yahoo.co.jp/yconnect/v2/jwks'.freeze
11
+ ISSUER = 'https://auth.login.yahoo.co.jp/yconnect/v2'.freeze
8
12
 
9
13
  option :name, 'yahoojp'
10
14
  option :client_options, {
@@ -75,9 +79,7 @@ module OmniAuth
75
79
 
76
80
  def id_token_claims
77
81
  return nil unless id_token
78
- # Signature verification is skipped because the id_token was received
79
- # directly from Yahoo's token endpoint over TLS (Authorization Code Flow).
80
- @id_token_claims ||= JSON::JWT.decode(id_token, :skip_verification)
82
+ @id_token_claims ||= verify_id_token!
81
83
  end
82
84
 
83
85
  def prune!(hash)
@@ -102,6 +104,28 @@ module OmniAuth
102
104
  full_host + script_name + callback_path
103
105
  end
104
106
 
107
+ private
108
+
109
+ def verify_id_token!
110
+ header = JSON::JWT.decode(id_token, :skip_verification).header
111
+ jwk = JSON::JWK::Set::Fetcher.fetch(JWKS_URI, kid: header['kid'])
112
+ claims = JSON::JWT.decode(id_token, jwk, [:RS256])
113
+ validate_id_token_claims!(claims)
114
+ claims
115
+ end
116
+
117
+ def validate_id_token_claims!(claims)
118
+ unless claims['iss'] == ISSUER
119
+ raise IdTokenValidationError, "Invalid issuer: #{claims['iss']}"
120
+ end
121
+ unless Array(claims['aud']).include?(client.id)
122
+ raise IdTokenValidationError, "Invalid audience: #{claims['aud']}"
123
+ end
124
+ if claims['exp'].nil? || Time.now.to_i > claims['exp'].to_i + 30
125
+ raise IdTokenValidationError, 'id_token has expired'
126
+ end
127
+ end
128
+
105
129
  end
106
130
  end
107
131
  end
@@ -1,5 +1,5 @@
1
1
  module OmniAuth
2
2
  module YahooJp
3
- VERSION = "1.0.0"
3
+ VERSION = "1.0.1"
4
4
  end
5
5
  end
@@ -26,9 +26,34 @@ RSpec.describe OmniAuth::Strategies::YahooJp do
26
26
  end
27
27
  end
28
28
 
29
+ describe 'constants' do
30
+ it 'has correct JWKS_URI' do
31
+ expect(described_class::JWKS_URI).to eq('https://auth.login.yahoo.co.jp/yconnect/v2/jwks')
32
+ end
33
+
34
+ it 'has correct ISSUER' do
35
+ expect(described_class::ISSUER).to eq('https://auth.login.yahoo.co.jp/yconnect/v2')
36
+ end
37
+ end
38
+
29
39
  context 'with access_token' do
30
- let(:jwt_payload) { { 'sub' => 'test123', 'name' => 'Test User', 'email' => 'test@example.com' } }
31
- let(:jwt_string) { JSON::JWT.new(jwt_payload).to_s }
40
+ let(:rsa_key) { OpenSSL::PKey::RSA.generate(2048) }
41
+ let(:kid) { 'test-kid-1' }
42
+ let(:jwt_payload) do
43
+ {
44
+ 'sub' => 'test123',
45
+ 'name' => 'Test User',
46
+ 'email' => 'test@example.com',
47
+ 'iss' => 'https://auth.login.yahoo.co.jp/yconnect/v2',
48
+ 'aud' => 'client_id',
49
+ 'exp' => Time.now.to_i + 3600
50
+ }
51
+ end
52
+ let(:jwt_string) do
53
+ jwt = JSON::JWT.new(jwt_payload)
54
+ jwt.kid = kid
55
+ jwt.sign(rsa_key, :RS256).to_s
56
+ end
32
57
  let(:access_token) do
33
58
  instance_double(
34
59
  OAuth2::AccessToken,
@@ -43,6 +68,13 @@ RSpec.describe OmniAuth::Strategies::YahooJp do
43
68
 
44
69
  before do
45
70
  allow(strategy).to receive(:access_token).and_return(access_token)
71
+ jwk = JSON::JWK.new(rsa_key, kid: kid)
72
+ stub_request(:get, 'https://auth.login.yahoo.co.jp/yconnect/v2/jwks')
73
+ .to_return(
74
+ status: 200,
75
+ body: { keys: [jwk] }.to_json,
76
+ headers: { 'Content-Type' => 'application/json' }
77
+ )
46
78
  end
47
79
 
48
80
  describe '#id_token' do
@@ -62,7 +94,7 @@ RSpec.describe OmniAuth::Strategies::YahooJp do
62
94
  end
63
95
 
64
96
  describe '#id_token_claims' do
65
- it 'decodes the JWT without verification' do
97
+ it 'decodes and verifies the JWT' do
66
98
  claims = strategy.id_token_claims
67
99
  expect(claims['sub']).to eq('test123')
68
100
  expect(claims['name']).to eq('Test User')
@@ -80,10 +112,159 @@ RSpec.describe OmniAuth::Strategies::YahooJp do
80
112
  expect(strategy.id_token_claims).to be_nil
81
113
  end
82
114
 
115
+ it 'returns nil without fetching JWKS when id_token is nil' do
116
+ allow(access_token).to receive(:params).and_return({})
117
+ strategy.id_token_claims
118
+ expect(WebMock).not_to have_requested(:get, 'https://auth.login.yahoo.co.jp/yconnect/v2/jwks')
119
+ end
120
+
83
121
  it 'raises on malformed id_token' do
84
122
  allow(access_token).to receive(:params).and_return({ 'id_token' => 'not-a-jwt' })
85
123
  expect { strategy.id_token_claims }.to raise_error(JSON::JWT::InvalidFormat)
86
124
  end
125
+
126
+ context 'with invalid signature' do
127
+ let(:wrong_key) { OpenSSL::PKey::RSA.generate(2048) }
128
+ let(:bad_jwt_string) do
129
+ jwt = JSON::JWT.new(jwt_payload)
130
+ jwt.kid = kid
131
+ jwt.sign(wrong_key, :RS256).to_s
132
+ end
133
+
134
+ before do
135
+ allow(access_token).to receive(:params).and_return({ 'id_token' => bad_jwt_string })
136
+ end
137
+
138
+ it 'raises an error' do
139
+ expect { strategy.id_token_claims }.to raise_error(JSON::JWS::VerificationFailed)
140
+ end
141
+ end
142
+
143
+ context 'with invalid issuer' do
144
+ let(:jwt_payload_bad_iss) { jwt_payload.merge('iss' => 'https://evil.example.com') }
145
+ let(:bad_iss_jwt) do
146
+ jwt = JSON::JWT.new(jwt_payload_bad_iss)
147
+ jwt.kid = kid
148
+ jwt.sign(rsa_key, :RS256).to_s
149
+ end
150
+
151
+ before do
152
+ allow(access_token).to receive(:params).and_return({ 'id_token' => bad_iss_jwt })
153
+ end
154
+
155
+ it 'raises IdTokenValidationError' do
156
+ expect { strategy.id_token_claims }.to raise_error(
157
+ described_class::IdTokenValidationError, /Invalid issuer/
158
+ )
159
+ end
160
+ end
161
+
162
+ context 'with issuer missing /yconnect/v2 suffix' do
163
+ let(:jwt_payload_wrong_iss) { jwt_payload.merge('iss' => 'https://auth.login.yahoo.co.jp') }
164
+ let(:wrong_iss_jwt) do
165
+ jwt = JSON::JWT.new(jwt_payload_wrong_iss)
166
+ jwt.kid = kid
167
+ jwt.sign(rsa_key, :RS256).to_s
168
+ end
169
+
170
+ before do
171
+ allow(access_token).to receive(:params).and_return({ 'id_token' => wrong_iss_jwt })
172
+ end
173
+
174
+ it 'raises IdTokenValidationError' do
175
+ expect { strategy.id_token_claims }.to raise_error(
176
+ described_class::IdTokenValidationError, /Invalid issuer/
177
+ )
178
+ end
179
+ end
180
+
181
+ context 'with invalid audience' do
182
+ let(:jwt_payload_bad_aud) { jwt_payload.merge('aud' => 'wrong_client_id') }
183
+ let(:bad_aud_jwt) do
184
+ jwt = JSON::JWT.new(jwt_payload_bad_aud)
185
+ jwt.kid = kid
186
+ jwt.sign(rsa_key, :RS256).to_s
187
+ end
188
+
189
+ before do
190
+ allow(access_token).to receive(:params).and_return({ 'id_token' => bad_aud_jwt })
191
+ end
192
+
193
+ it 'raises IdTokenValidationError' do
194
+ expect { strategy.id_token_claims }.to raise_error(
195
+ described_class::IdTokenValidationError, /Invalid audience/
196
+ )
197
+ end
198
+ end
199
+
200
+ context 'with audience as array including client_id' do
201
+ let(:jwt_payload_array_aud) { jwt_payload.merge('aud' => ['other_client', 'client_id']) }
202
+ let(:array_aud_jwt) do
203
+ jwt = JSON::JWT.new(jwt_payload_array_aud)
204
+ jwt.kid = kid
205
+ jwt.sign(rsa_key, :RS256).to_s
206
+ end
207
+
208
+ before do
209
+ allow(access_token).to receive(:params).and_return({ 'id_token' => array_aud_jwt })
210
+ end
211
+
212
+ it 'accepts the token' do
213
+ expect { strategy.id_token_claims }.not_to raise_error
214
+ end
215
+ end
216
+
217
+ context 'with expired token' do
218
+ let(:jwt_payload_expired) { jwt_payload.merge('exp' => Time.now.to_i - 60) }
219
+ let(:expired_jwt) do
220
+ jwt = JSON::JWT.new(jwt_payload_expired)
221
+ jwt.kid = kid
222
+ jwt.sign(rsa_key, :RS256).to_s
223
+ end
224
+
225
+ before do
226
+ allow(access_token).to receive(:params).and_return({ 'id_token' => expired_jwt })
227
+ end
228
+
229
+ it 'raises IdTokenValidationError' do
230
+ expect { strategy.id_token_claims }.to raise_error(
231
+ described_class::IdTokenValidationError, /expired/
232
+ )
233
+ end
234
+ end
235
+
236
+ context 'with token within 30-second leeway' do
237
+ let(:jwt_payload_just_expired) { jwt_payload.merge('exp' => Time.now.to_i - 10) }
238
+ let(:leeway_jwt) do
239
+ jwt = JSON::JWT.new(jwt_payload_just_expired)
240
+ jwt.kid = kid
241
+ jwt.sign(rsa_key, :RS256).to_s
242
+ end
243
+
244
+ before do
245
+ allow(access_token).to receive(:params).and_return({ 'id_token' => leeway_jwt })
246
+ end
247
+
248
+ it 'accepts the token' do
249
+ expect { strategy.id_token_claims }.not_to raise_error
250
+ end
251
+ end
252
+
253
+ context 'with kid not found in JWKS' do
254
+ let(:jwt_with_unknown_kid) do
255
+ jwt = JSON::JWT.new(jwt_payload)
256
+ jwt.kid = 'unknown-kid'
257
+ jwt.sign(rsa_key, :RS256).to_s
258
+ end
259
+
260
+ before do
261
+ allow(access_token).to receive(:params).and_return({ 'id_token' => jwt_with_unknown_kid })
262
+ end
263
+
264
+ it 'raises an error' do
265
+ expect { strategy.id_token_claims }.to raise_error(StandardError)
266
+ end
267
+ end
87
268
  end
88
269
 
89
270
  describe '#raw_info' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniauth-yahoojp
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - mikanmarusan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-08 00:00:00.000000000 Z
11
+ date: 2026-03-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: omniauth