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 +4 -4
- data/README.md +9 -2
- data/lib/omniauth/strategies/yahoojp.rb +27 -3
- data/lib/omniauth-yahoojp/version.rb +1 -1
- data/spec/omniauth/strategies/yahoojp_spec.rb +184 -3
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cef0fc73e6ad6338ec0f4a4180774ab663b9a0846ad2e7658f9a4c3fd395b454
|
|
4
|
+
data.tar.gz: 37e8e9ef4282fc449ea7d489ef4a43ea4ab0d231c7ddda5a6163b932c899d7ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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`
|
|
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
|
-
|
|
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
|
|
@@ -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(:
|
|
31
|
-
let(:
|
|
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
|
|
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.
|
|
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-
|
|
11
|
+
date: 2026-03-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: omniauth
|