omniauth-yahoojp 0.2.1 → 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
- SHA1:
3
- metadata.gz: b7f78180ef96d2a2e46a852420a8dbb7d530da3f
4
- data.tar.gz: f73968ec635fee1510aae21153ee3d862ae4186f
2
+ SHA256:
3
+ metadata.gz: cef0fc73e6ad6338ec0f4a4180774ab663b9a0846ad2e7658f9a4c3fd395b454
4
+ data.tar.gz: 37e8e9ef4282fc449ea7d489ef4a43ea4ab0d231c7ddda5a6163b932c899d7ac
5
5
  SHA512:
6
- metadata.gz: 4b785600fc24397488dbfd26c4725567480a51b0ee894140273efbb1213490c15f9009cce632224ec2e619820555125d11935fb34f97c4c6d00ebd84c6366f36
7
- data.tar.gz: cb83e33ffab80a04c59e8441167b972d88e8d6f15dca4a3bb3e359e6ada394ab3593d09e79ad4c557f1dd91e33b820be17022d167b128ac4622a61de51338272
6
+ metadata.gz: cf6a1d9162d5855cd7ed66074b9f5d275312024602fb956a9e367e522aeaf36d3637c73856c3cd94b3dcc5f0d53ea01697aec948370689b766d0b6f6890a29c3
7
+ data.tar.gz: 84aa02e550b3fd6bbc286912be6181b484b2d7d1db6ab6ff8792a9819575b08b0c77e406b56045fdac3073cb4900500eb07a7ab91641de909ffb1881d8953043
@@ -0,0 +1,78 @@
1
+ name: Claude Code Review
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize]
6
+ # Optional: Only run on specific file changes
7
+ # paths:
8
+ # - "src/**/*.ts"
9
+ # - "src/**/*.tsx"
10
+ # - "src/**/*.js"
11
+ # - "src/**/*.jsx"
12
+
13
+ jobs:
14
+ claude-review:
15
+ # Optional: Filter by PR author
16
+ # if: |
17
+ # github.event.pull_request.user.login == 'external-contributor' ||
18
+ # github.event.pull_request.user.login == 'new-developer' ||
19
+ # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
20
+
21
+ runs-on: ubuntu-latest
22
+ permissions:
23
+ contents: read
24
+ pull-requests: read
25
+ issues: read
26
+ id-token: write
27
+
28
+ steps:
29
+ - name: Checkout repository
30
+ uses: actions/checkout@v4
31
+ with:
32
+ fetch-depth: 1
33
+
34
+ - name: Run Claude Code Review
35
+ id: claude-review
36
+ uses: anthropics/claude-code-action@beta
37
+ with:
38
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
39
+
40
+ # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
41
+ # model: "claude-opus-4-20250514"
42
+
43
+ # Direct prompt for automated review (no @claude mention needed)
44
+ direct_prompt: |
45
+ Please review this pull request and provide feedback on:
46
+ - Code quality and best practices
47
+ - Potential bugs or issues
48
+ - Performance considerations
49
+ - Security concerns
50
+ - Test coverage
51
+
52
+ Be constructive and helpful in your feedback.
53
+
54
+ # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
55
+ # use_sticky_comment: true
56
+
57
+ # Optional: Customize review based on file types
58
+ # direct_prompt: |
59
+ # Review this PR focusing on:
60
+ # - For TypeScript files: Type safety and proper interface usage
61
+ # - For API endpoints: Security, input validation, and error handling
62
+ # - For React components: Performance, accessibility, and best practices
63
+ # - For tests: Coverage, edge cases, and test quality
64
+
65
+ # Optional: Different prompts for different authors
66
+ # direct_prompt: |
67
+ # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
68
+ # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' ||
69
+ # 'Please provide a thorough code review focusing on our coding standards and best practices.' }}
70
+
71
+ # Optional: Add specific tools for running tests or linting
72
+ # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"
73
+
74
+ # Optional: Skip review for certain conditions
75
+ # if: |
76
+ # !contains(github.event.pull_request.title, '[skip-review]') &&
77
+ # !contains(github.event.pull_request.title, '[WIP]')
78
+
@@ -0,0 +1,64 @@
1
+ name: Claude Code
2
+
3
+ on:
4
+ issue_comment:
5
+ types: [created]
6
+ pull_request_review_comment:
7
+ types: [created]
8
+ issues:
9
+ types: [opened, assigned]
10
+ pull_request_review:
11
+ types: [submitted]
12
+
13
+ jobs:
14
+ claude:
15
+ if: |
16
+ (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
17
+ (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
18
+ (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19
+ (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
20
+ runs-on: ubuntu-latest
21
+ permissions:
22
+ contents: read
23
+ pull-requests: read
24
+ issues: read
25
+ id-token: write
26
+ actions: read # Required for Claude to read CI results on PRs
27
+ steps:
28
+ - name: Checkout repository
29
+ uses: actions/checkout@v4
30
+ with:
31
+ fetch-depth: 1
32
+
33
+ - name: Run Claude Code
34
+ id: claude
35
+ uses: anthropics/claude-code-action@beta
36
+ with:
37
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38
+
39
+ # This is an optional setting that allows Claude to read CI results on PRs
40
+ additional_permissions: |
41
+ actions: read
42
+
43
+ # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
44
+ # model: "claude-opus-4-20250514"
45
+
46
+ # Optional: Customize the trigger phrase (default: @claude)
47
+ # trigger_phrase: "/claude"
48
+
49
+ # Optional: Trigger when specific user is assigned to an issue
50
+ # assignee_trigger: "claude-bot"
51
+
52
+ # Optional: Allow Claude to run specific commands
53
+ # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
54
+
55
+ # Optional: Add custom instructions for Claude to customize its behavior for your project
56
+ # custom_instructions: |
57
+ # Follow our coding standards
58
+ # Ensure all new code has tests
59
+ # Use TypeScript for new files
60
+
61
+ # Optional: Custom environment variables for Claude
62
+ # claude_env: |
63
+ # NODE_ENV: test
64
+
data/CLAUDE.md ADDED
@@ -0,0 +1,49 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Development Commands
6
+
7
+ ### Testing
8
+ - `rake spec` or `rspec` - Run all tests
9
+ - `rspec spec/omniauth/strategies/yahoojp_spec.rb` - Run specific test file
10
+ - `rake` - Default task runs specs
11
+
12
+ ### Development Tools
13
+ - `guard` - File watcher for automatic test running (configured via Guardfile)
14
+ - `bundle install` - Install gem dependencies
15
+
16
+ ## Architecture
17
+
18
+ This is a Ruby gem that implements an OmniAuth strategy for Yahoo! JAPAN's YConnect OAuth 2.0 service.
19
+
20
+ ### Core Components
21
+
22
+ **Main Strategy**: `lib/omniauth/strategies/yahoojp.rb`
23
+ - Inherits from `OmniAuth::Strategies::OAuth2`
24
+ - Implements Yahoo! JAPAN YConnect v2 API endpoints
25
+ - Handles OAuth 2.0 authorization code flow with basic auth for token exchange
26
+ - Supports YConnect-specific parameters: display, prompt, scope, bail
27
+
28
+ **Key Implementation Details**:
29
+ - Uses `https://auth.login.yahoo.co.jp` as OAuth provider
30
+ - YConnect v2 endpoints: `/yconnect/v2/authorization` and `/yconnect/v2/token`
31
+ - User info endpoint: `https://userinfo.yahooapis.jp/yconnect/v2/attribute`
32
+ - Custom `build_access_token` method for Yahoo-specific auth headers
33
+ - Supports Japanese locale-specific fields (kana, kanji names)
34
+
35
+ ### File Structure
36
+ - `lib/omniauth-yahoojp.rb` - Main entry point, requires the strategy
37
+ - `lib/omniauth-yahoojp/version.rb` - Gem version
38
+ - `lib/omniauth/strategies/yahoojp.rb` - Strategy implementation
39
+ - `spec/` - RSpec tests
40
+
41
+ ### Dependencies
42
+ - `omniauth` and `omniauth-oauth2` for OAuth framework
43
+ - `httpauth` for HTTP basic authentication headers
44
+
45
+ ## Downstream Test Project
46
+
47
+ - [`omniauth-yahoojp-tester-rails5`](https://github.com/mikanmarusan/omniauth-yahoojp-tester-rails5) is a Rails 5 app that consumes this gem for integration testing.
48
+ - Its CLAUDE.md documents the exact API surface used and cross-project update rules.
49
+ - Use `/add-dir ../omniauth-yahoojp-tester-rails5` to add it for cross-project awareness when making API changes.
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source 'http://rubygems.org'
1
+ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in omniauth-yahoojp.gemspec
4
4
  gemspec
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **These notes are based on master, please see tags for README pertaining to specific releases.**
4
4
 
5
- This is the official OmniAuth strategy for authenticating to Yahoo! JAPAN( [YConnect](http://developer.yahoo.co.jp/yconnect/v2/) ).
5
+ This is the OmniAuth strategy for authenticating to Yahoo! JAPAN( [YConnect](http://developer.yahoo.co.jp/yconnect/v2/) ).
6
6
  To use it, you'll need to sign up for a YConnect Client ID and Secret
7
7
  on the [Yahoo! JAPAN Developer Network](https://e.developer.yahoo.co.jp/dashboard/).
8
8
 
@@ -22,13 +22,14 @@ Then `bundle install`.
22
22
 
23
23
  `OmniAuth::Strategies::YahooJp` is simply a Rack middleware. Read the OmniAuth docs for detailed instructions: https://github.com/intridea/omniauth.
24
24
 
25
- YConnect API v2 lets you set scopes to provide granular access to different types of data:
25
+ YConnect API v2 lets you set scopes to provide granular access to different types of data:
26
26
 
27
27
  ```ruby
28
28
  Rails.application.config.middleware.use OmniAuth::Builder do
29
- provider :yahoojp, ENV['YAHOOJP_KEY'], ENV['YAHOOJP_SECRET'],
29
+ provider :yahoojp, ENV['YAHOOJP_KEY'], ENV['YAHOOJP_SECRET'],
30
30
  {
31
- scope: "openid profile email address"
31
+ scope: "openid profile email address",
32
+ userinfo_access: true # default: true
32
33
  }
33
34
  end
34
35
  ```
@@ -50,6 +51,31 @@ Rails.application.config.middleware.use OmniAuth::Builder do
50
51
  end
51
52
  ```
52
53
 
54
+ ### `userinfo_access` Option
55
+
56
+ Controls how user profile information is retrieved.
57
+
58
+ - `userinfo_access: true` (default) — Calls the UserInfo API (`https://userinfo.yahooapis.jp/yconnect/v2/attribute`) to retrieve full profile information including name variants (kana, kanji), gender, locale, birthdate, nickname, picture, email, and address.
59
+ - `userinfo_access: false` — Skips the UserInfo API call and extracts profile information from the `id_token` claims instead. This is useful when your application does not have access to the UserInfo API.
60
+
61
+ > **Note:** The fields available in `id_token` claims may differ from those returned by the UserInfo API. The `id_token` typically contains a subset of profile fields depending on the requested scopes. Yahoo! JAPAN has made the UserInfo API access-restricted, so some applications may need to rely on `id_token` claims.
62
+
63
+ ### ID Token
64
+
65
+ When the `openid` scope is requested, the strategy automatically captures the `id_token` returned by Yahoo! JAPAN's token endpoint.
66
+
67
+ - `credentials.id_token` — The raw JWT string as returned from the token endpoint.
68
+ - `extra.id_token_claims` — The decoded and verified claims hash from the `id_token`.
69
+
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.
78
+
53
79
  ### API Version
54
80
 
55
81
  OmniAuth YahooJp uses versioned API endpoints by default (current v2). You can configure a different version via `client_options` hash passed to `provider`, specifically you should change the version in the `site` and `authorize_url` parameters. For example, to change to v1:
@@ -67,6 +93,52 @@ Rails.application.config.middleware.use OmniAuth::Builder do
67
93
  end
68
94
  ```
69
95
 
96
+ ## Auth Hash
97
+
98
+ Here is an example of the auth hash available in `request.env['omniauth.auth']`:
99
+
100
+ ```ruby
101
+ {
102
+ provider: "yahoojp",
103
+ uid: "abcdefg",
104
+ info: {
105
+ sub: "abcdefg",
106
+ name: "山田 太郎",
107
+ given_name: "太郎",
108
+ given_name_ja_kana_jp: "タロウ",
109
+ given_name_ja_hani_jp: "太郎",
110
+ family_name: "山田",
111
+ family_name_ja_kana_jp: "ヤマダ",
112
+ family_name_ja_hani_jp: "山田",
113
+ gender: "male",
114
+ locale: "ja-JP",
115
+ email: "example@yahoo.co.jp",
116
+ email_verified: true
117
+ },
118
+ credentials: {
119
+ token: "ACCESS_TOKEN",
120
+ refresh_token: "REFRESH_TOKEN",
121
+ expires_at: 1496120719,
122
+ expires: true,
123
+ id_token: "eyJhbGciOiJSUzI1NiIs..."
124
+ },
125
+ extra: {
126
+ raw_info: { ... }, # Full response from UserInfo API (or id_token claims)
127
+ id_token: "eyJhbGciOiJSUzI1NiIs...", # Raw JWT string
128
+ id_token_claims: { # Decoded id_token claims
129
+ iss: "https://auth.login.yahoo.co.jp/yconnect/v2",
130
+ sub: "abcdefg",
131
+ aud: ["YOUR_CLIENT_ID"],
132
+ exp: 1496120719,
133
+ iat: 1496117119,
134
+ nonce: "..."
135
+ }
136
+ }
137
+ }
138
+ ```
139
+
140
+ > **Note:** Available fields in `info` and `extra.raw_info` depend on the requested scopes and the `userinfo_access` setting.
141
+
70
142
  ## License
71
143
 
72
144
  Copyright (c) 2013 by mikanmarusan
@@ -1,9 +1,14 @@
1
1
  require 'omniauth-oauth2'
2
2
  require 'httpauth'
3
+ require 'json/jwt'
3
4
 
4
5
  module OmniAuth
5
6
  module Strategies
6
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
7
12
 
8
13
  option :name, 'yahoojp'
9
14
  option :client_options, {
@@ -14,10 +19,7 @@ module OmniAuth
14
19
  }
15
20
 
16
21
  option :authorize_options, [:display, :prompt, :scope, :bail]
17
-
18
- def request_phase
19
- super
20
- end
22
+ option :userinfo_access, true
21
23
 
22
24
  uid { raw_info['sub'] }
23
25
 
@@ -39,19 +41,45 @@ module OmniAuth
39
41
  :picture => raw_info['picture'],
40
42
  :email => raw_info['email'],
41
43
  :email_verified => raw_info['email_verified'],
42
- :address => raw_info['address'],
44
+ :address => raw_info['address'],
43
45
  })
44
46
  end
45
47
 
46
48
  extra do
47
49
  hash = {}
48
50
  hash[:raw_info] = raw_info unless skip_info?
51
+ hash[:id_token] = id_token if id_token
52
+ hash[:id_token_claims] = id_token_claims if id_token
49
53
  prune! hash
50
54
  end
51
55
 
56
+ credentials do
57
+ hash = {'token' => access_token.token}
58
+ hash['refresh_token'] = access_token.refresh_token if access_token.refresh_token
59
+ hash['expires_at'] = access_token.expires_at if access_token.expires?
60
+ hash['expires'] = access_token.expires?
61
+ hash['id_token'] = id_token if id_token
62
+ hash
63
+ end
64
+
52
65
  def raw_info
53
- access_token.options[:mode] = :header
54
- @raw_info ||= access_token.get('https://userinfo.yahooapis.jp/yconnect/v2/attribute').parsed
66
+ @raw_info ||= if options.userinfo_access
67
+ access_token.options[:mode] = :header
68
+ access_token.get('https://userinfo.yahooapis.jp/yconnect/v2/attribute').parsed
69
+ elsif id_token
70
+ id_token_claims
71
+ else
72
+ {}
73
+ end
74
+ end
75
+
76
+ def id_token
77
+ @id_token ||= access_token&.params&.dig('id_token').presence
78
+ end
79
+
80
+ def id_token_claims
81
+ return nil unless id_token
82
+ @id_token_claims ||= verify_id_token!
55
83
  end
56
84
 
57
85
  def prune!(hash)
@@ -69,13 +97,35 @@ module OmniAuth
69
97
  :headers => {'Authorization' => HTTPAuth::Basic.pack_authorization(client.id, client.secret)}
70
98
  }
71
99
 
72
- client.get_token(token_params);
100
+ client.get_token(token_params)
73
101
  end
74
102
 
75
103
  def callback_url
76
104
  full_host + script_name + callback_path
77
105
  end
78
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
+
79
129
  end
80
130
  end
81
131
  end
@@ -1,5 +1,5 @@
1
1
  module OmniAuth
2
2
  module YahooJp
3
- VERSION = "0.2.1"
3
+ VERSION = "1.0.1"
4
4
  end
5
5
  end
@@ -4,8 +4,8 @@ require File.expand_path('../lib/omniauth-yahoojp/version', __FILE__)
4
4
  Gem::Specification.new do |gem|
5
5
  gem.authors = ["mikanmarusan"]
6
6
  gem.email = ["chiakifujimon@gmail.com"]
7
- gem.description = %q{Official OmniAuth strategy for Yahoo! JAPAN.}
8
- gem.summary = %q{Official OmniAuth strategy for Yahoo! JAPAN.}
7
+ gem.description = %q{OmniAuth strategy for Yahoo! JAPAN.}
8
+ gem.summary = %q{OmniAuth strategy for Yahoo! JAPAN.}
9
9
  gem.homepage = "https://github.com/mikanmarusan/omniauth-yahoojp"
10
10
 
11
11
  gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
@@ -14,12 +14,13 @@ Gem::Specification.new do |gem|
14
14
  gem.name = "omniauth-yahoojp"
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = OmniAuth::YahooJp::VERSION
17
- gem.licenses = "MIT"
17
+ gem.licenses = ["MIT"]
18
18
 
19
19
  gem.add_dependency 'omniauth', '>= 1.0'
20
20
  gem.add_dependency 'omniauth-oauth2', '>= 1.1'
21
21
  gem.add_dependency 'httpauth'
22
- gem.add_development_dependency 'rspec', '~> 2.7'
22
+ gem.add_dependency 'json-jwt', '>= 1.16.6'
23
+ gem.add_development_dependency 'rspec', '~> 3.0'
23
24
  gem.add_development_dependency 'rack-test'
24
25
  gem.add_development_dependency 'simplecov'
25
26
  gem.add_development_dependency 'webmock'
@@ -1,23 +1,470 @@
1
1
  require 'spec_helper'
2
2
  require 'omniauth-yahoojp'
3
+ require 'json/jwt'
3
4
 
4
- describe OmniAuth::Strategies::YahooJp do
5
+ RSpec.describe OmniAuth::Strategies::YahooJp do
6
+ let(:app) { lambda { |_env| [200, {}, ['Hello']] } }
7
+ let(:strategy) { described_class.new(app, 'client_id', 'client_secret') }
5
8
 
6
- subject do
7
- OmniAuth::Strategies::YahooJp.new({})
9
+ describe 'client options' do
10
+ it 'has correct site' do
11
+ expect(strategy.options.client_options.site).to eq('https://auth.login.yahoo.co.jp')
12
+ end
13
+
14
+ it 'has correct authorize url' do
15
+ expect(strategy.options.client_options.authorize_url).to eq('/yconnect/v2/authorization')
16
+ end
17
+
18
+ it 'has correct token url' do
19
+ expect(strategy.options.client_options.token_url).to eq('/yconnect/v2/token')
20
+ end
8
21
  end
9
22
 
10
- context "client options" do
11
- it 'should have correct site' do
12
- subject.options.client_options.site.should eq("https://auth.login.yahoo.co.jp")
23
+ describe 'default options' do
24
+ it 'has userinfo_access enabled by default' do
25
+ expect(strategy.options.userinfo_access).to be true
13
26
  end
27
+ end
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
14
38
 
15
- it 'should have correct authorize url' do
16
- subject.options.client_options.authorize_url.should eq('/yconnect/v1/authorization')
39
+ context 'with access_token' do
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
+ }
17
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
57
+ let(:access_token) do
58
+ instance_double(
59
+ OAuth2::AccessToken,
60
+ token: 'mock_token',
61
+ refresh_token: 'mock_refresh',
62
+ expires?: true,
63
+ expires_at: 1234567890,
64
+ params: { 'id_token' => jwt_string },
65
+ options: {}
66
+ )
67
+ end
68
+
69
+ before do
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
+ )
78
+ end
79
+
80
+ describe '#id_token' do
81
+ it 'returns the id_token from access_token params' do
82
+ expect(strategy.id_token).to eq(jwt_string)
83
+ end
84
+
85
+ it 'returns nil when no id_token in params' do
86
+ allow(access_token).to receive(:params).and_return({})
87
+ expect(strategy.id_token).to be_nil
88
+ end
89
+
90
+ it 'returns nil when id_token is empty string' do
91
+ allow(access_token).to receive(:params).and_return({ 'id_token' => '' })
92
+ expect(strategy.id_token).to be_nil
93
+ end
94
+ end
95
+
96
+ describe '#id_token_claims' do
97
+ it 'decodes and verifies the JWT' do
98
+ claims = strategy.id_token_claims
99
+ expect(claims['sub']).to eq('test123')
100
+ expect(claims['name']).to eq('Test User')
101
+ expect(claims['email']).to eq('test@example.com')
102
+ end
103
+
104
+ it 'memoizes the result' do
105
+ first_call = strategy.id_token_claims
106
+ second_call = strategy.id_token_claims
107
+ expect(first_call).to equal(second_call)
108
+ end
109
+
110
+ it 'returns nil when id_token is nil' do
111
+ allow(access_token).to receive(:params).and_return({})
112
+ expect(strategy.id_token_claims).to be_nil
113
+ end
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
+
121
+ it 'raises on malformed id_token' do
122
+ allow(access_token).to receive(:params).and_return({ 'id_token' => 'not-a-jwt' })
123
+ expect { strategy.id_token_claims }.to raise_error(JSON::JWT::InvalidFormat)
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
268
+ end
269
+
270
+ describe '#raw_info' do
271
+ context 'with userinfo_access: true' do
272
+ let(:userinfo_response) do
273
+ instance_double(OAuth2::Response, parsed: {
274
+ 'sub' => 'user456',
275
+ 'name' => 'Yahoo User',
276
+ 'email' => 'yahoo@example.com',
277
+ 'address' => { 'country' => 'JP' }
278
+ })
279
+ end
280
+
281
+ before do
282
+ allow(access_token).to receive(:get)
283
+ .with('https://userinfo.yahooapis.jp/yconnect/v2/attribute')
284
+ .and_return(userinfo_response)
285
+ end
286
+
287
+ it 'calls the UserInfo API' do
288
+ strategy.raw_info
289
+ expect(access_token).to have_received(:get)
290
+ .with('https://userinfo.yahooapis.jp/yconnect/v2/attribute')
291
+ end
292
+
293
+ it 'sets access_token mode to header' do
294
+ strategy.raw_info
295
+ expect(access_token.options[:mode]).to eq(:header)
296
+ end
297
+
298
+ it 'returns parsed UserInfo response' do
299
+ expect(strategy.raw_info['sub']).to eq('user456')
300
+ expect(strategy.raw_info['name']).to eq('Yahoo User')
301
+ end
302
+ end
303
+
304
+ context 'with userinfo_access: false and id_token present' do
305
+ before do
306
+ strategy.options[:userinfo_access] = false
307
+ end
308
+
309
+ it 'returns id_token_claims' do
310
+ expect(strategy.raw_info['sub']).to eq('test123')
311
+ expect(strategy.raw_info['name']).to eq('Test User')
312
+ end
313
+
314
+ it 'does not call the UserInfo API' do
315
+ expect(access_token).not_to receive(:get)
316
+ strategy.raw_info
317
+ end
318
+ end
319
+
320
+ context 'with userinfo_access: false and no id_token' do
321
+ before do
322
+ strategy.options[:userinfo_access] = false
323
+ allow(access_token).to receive(:params).and_return({})
324
+ end
325
+
326
+ it 'returns empty hash' do
327
+ expect(strategy.raw_info).to eq({})
328
+ end
329
+ end
330
+ end
331
+
332
+ describe '#info' do
333
+ let(:full_profile) do
334
+ {
335
+ 'sub' => 'user456',
336
+ 'name' => 'Yahoo User',
337
+ 'given_name' => 'Taro',
338
+ 'given_name#ja-Kana-JP' => 'タロウ',
339
+ 'given_name#ja-Hani-JP' => '太郎',
340
+ 'family_name' => 'Yamada',
341
+ 'family_name#ja-Kana-JP' => 'ヤマダ',
342
+ 'family_name#ja-Hani-JP' => '山田',
343
+ 'gender' => 'male',
344
+ 'zoneinfo' => 'Asia/Tokyo',
345
+ 'locale' => 'ja-JP',
346
+ 'birthdate' => '1990-01-01',
347
+ 'nickname' => 'taro',
348
+ 'picture' => 'https://example.com/photo.jpg',
349
+ 'email' => 'taro@example.com',
350
+ 'email_verified' => true,
351
+ 'address' => { 'country' => 'JP', 'region' => 'Tokyo' }
352
+ }
353
+ end
354
+ let(:userinfo_response) do
355
+ instance_double(OAuth2::Response, parsed: full_profile)
356
+ end
357
+
358
+ before do
359
+ allow(access_token).to receive(:get)
360
+ .with('https://userinfo.yahooapis.jp/yconnect/v2/attribute')
361
+ .and_return(userinfo_response)
362
+ end
363
+
364
+ it 'maps all standard fields' do
365
+ info = strategy.info
366
+ expect(info[:sub]).to eq('user456')
367
+ expect(info[:name]).to eq('Yahoo User')
368
+ expect(info[:given_name]).to eq('Taro')
369
+ expect(info[:family_name]).to eq('Yamada')
370
+ expect(info[:email]).to eq('taro@example.com')
371
+ expect(info[:nickname]).to eq('taro')
372
+ expect(info[:picture]).to eq('https://example.com/photo.jpg')
373
+ end
374
+
375
+ it 'maps Japanese locale-specific fields' do
376
+ info = strategy.info
377
+ expect(info[:given_name_ja_kana_jp]).to eq('タロウ')
378
+ expect(info[:given_name_ja_hani_jp]).to eq('太郎')
379
+ expect(info[:family_name_ja_kana_jp]).to eq('ヤマダ')
380
+ expect(info[:family_name_ja_hani_jp]).to eq('山田')
381
+ end
382
+
383
+ it 'maps address as nested hash' do
384
+ info = strategy.info
385
+ expect(info[:address]['country']).to eq('JP')
386
+ end
387
+
388
+ it 'prunes nil values' do
389
+ allow(userinfo_response).to receive(:parsed).and_return({ 'sub' => 'user456' })
390
+ info = strategy.info
391
+ expect(info).not_to have_key(:name)
392
+ expect(info).not_to have_key(:email)
393
+ end
394
+ end
395
+
396
+ describe '#uid' do
397
+ context 'with userinfo_access: true' do
398
+ let(:userinfo_response) do
399
+ instance_double(OAuth2::Response, parsed: { 'sub' => 'user456' })
400
+ end
401
+
402
+ before do
403
+ allow(access_token).to receive(:get)
404
+ .with('https://userinfo.yahooapis.jp/yconnect/v2/attribute')
405
+ .and_return(userinfo_response)
406
+ end
407
+
408
+ it 'returns sub from UserInfo' do
409
+ expect(strategy.uid).to eq('user456')
410
+ end
411
+ end
412
+
413
+ context 'with userinfo_access: false' do
414
+ before do
415
+ strategy.options[:userinfo_access] = false
416
+ end
417
+
418
+ it 'returns sub from id_token' do
419
+ expect(strategy.uid).to eq('test123')
420
+ end
421
+ end
422
+ end
423
+
424
+ describe '#credentials' do
425
+ it 'includes token' do
426
+ expect(strategy.credentials['token']).to eq('mock_token')
427
+ end
428
+
429
+ it 'includes refresh_token' do
430
+ expect(strategy.credentials['refresh_token']).to eq('mock_refresh')
431
+ end
432
+
433
+ it 'includes expires_at' do
434
+ expect(strategy.credentials['expires_at']).to eq(1234567890)
435
+ end
436
+
437
+ it 'includes expires flag' do
438
+ expect(strategy.credentials['expires']).to be true
439
+ end
440
+
441
+ it 'includes id_token' do
442
+ expect(strategy.credentials['id_token']).to eq(jwt_string)
443
+ end
444
+
445
+ it 'omits id_token when not present' do
446
+ allow(access_token).to receive(:params).and_return({})
447
+ expect(strategy.credentials).not_to have_key('id_token')
448
+ end
449
+ end
450
+
451
+ describe '#extra' do
452
+ before do
453
+ strategy.options[:userinfo_access] = false
454
+ end
455
+
456
+ it 'includes id_token' do
457
+ expect(strategy.extra[:id_token]).to eq(jwt_string)
458
+ end
459
+
460
+ it 'includes id_token_claims' do
461
+ claims = strategy.extra[:id_token_claims]
462
+ expect(claims['sub']).to eq('test123')
463
+ end
18
464
 
19
- it 'should have correct token url' do
20
- subject.options.client_options.token_url.should eq('/yconnect/v1/token')
465
+ it 'includes raw_info' do
466
+ expect(strategy.extra[:raw_info]).not_to be_nil
467
+ end
21
468
  end
22
469
  end
23
470
  end
data/spec/spec_helper.rb CHANGED
@@ -11,6 +11,4 @@ require 'omniauth-yahoojp'
11
11
  RSpec.configure do |config|
12
12
  config.include WebMock::API
13
13
  config.include Rack::Test::Methods
14
- config.extend OmniAuth::Test::StrategyMacros, :type => :strategy
15
14
  end
16
-
metadata CHANGED
@@ -1,122 +1,139 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniauth-yahoojp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
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: 2019-03-01 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
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '>='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - '>='
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: omniauth-oauth2
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - '>='
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '1.1'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - '>='
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.1'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: httpauth
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - '>='
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '0'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - '>='
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: json-jwt
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 1.16.6
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 1.16.6
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rspec
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
- - - ~>
73
+ - - "~>"
60
74
  - !ruby/object:Gem::Version
61
- version: '2.7'
75
+ version: '3.0'
62
76
  type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
- - - ~>
80
+ - - "~>"
67
81
  - !ruby/object:Gem::Version
68
- version: '2.7'
82
+ version: '3.0'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: rack-test
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
- - - '>='
87
+ - - ">="
74
88
  - !ruby/object:Gem::Version
75
89
  version: '0'
76
90
  type: :development
77
91
  prerelease: false
78
92
  version_requirements: !ruby/object:Gem::Requirement
79
93
  requirements:
80
- - - '>='
94
+ - - ">="
81
95
  - !ruby/object:Gem::Version
82
96
  version: '0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: simplecov
85
99
  requirement: !ruby/object:Gem::Requirement
86
100
  requirements:
87
- - - '>='
101
+ - - ">="
88
102
  - !ruby/object:Gem::Version
89
103
  version: '0'
90
104
  type: :development
91
105
  prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
- - - '>='
108
+ - - ">="
95
109
  - !ruby/object:Gem::Version
96
110
  version: '0'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: webmock
99
113
  requirement: !ruby/object:Gem::Requirement
100
114
  requirements:
101
- - - '>='
115
+ - - ">="
102
116
  - !ruby/object:Gem::Version
103
117
  version: '0'
104
118
  type: :development
105
119
  prerelease: false
106
120
  version_requirements: !ruby/object:Gem::Requirement
107
121
  requirements:
108
- - - '>='
122
+ - - ">="
109
123
  - !ruby/object:Gem::Version
110
124
  version: '0'
111
- description: Official OmniAuth strategy for Yahoo! JAPAN.
125
+ description: OmniAuth strategy for Yahoo! JAPAN.
112
126
  email:
113
127
  - chiakifujimon@gmail.com
114
128
  executables: []
115
129
  extensions: []
116
130
  extra_rdoc_files: []
117
131
  files:
118
- - .gitignore
119
- - .rspec
132
+ - ".github/workflows/claude-code-review.yml"
133
+ - ".github/workflows/claude.yml"
134
+ - ".gitignore"
135
+ - ".rspec"
136
+ - CLAUDE.md
120
137
  - Gemfile
121
138
  - Guardfile
122
139
  - README.md
@@ -137,20 +154,19 @@ require_paths:
137
154
  - lib
138
155
  required_ruby_version: !ruby/object:Gem::Requirement
139
156
  requirements:
140
- - - '>='
157
+ - - ">="
141
158
  - !ruby/object:Gem::Version
142
159
  version: '0'
143
160
  required_rubygems_version: !ruby/object:Gem::Requirement
144
161
  requirements:
145
- - - '>='
162
+ - - ">="
146
163
  - !ruby/object:Gem::Version
147
164
  version: '0'
148
165
  requirements: []
149
- rubyforge_project:
150
- rubygems_version: 2.0.14.1
166
+ rubygems_version: 3.0.3.1
151
167
  signing_key:
152
168
  specification_version: 4
153
- summary: Official OmniAuth strategy for Yahoo! JAPAN.
169
+ summary: OmniAuth strategy for Yahoo! JAPAN.
154
170
  test_files:
155
171
  - spec/omniauth/strategies/yahoojp_spec.rb
156
172
  - spec/spec_helper.rb