From 194b9c6526ec315a0596f9b189d8028783d6e32f Mon Sep 17 00:00:00 2001 From: HolyGrail Date: Sun, 3 Aug 2025 01:12:10 +0900 Subject: [PATCH 1/2] feat: add OAuth2 scope constants and validation - Add AVAILABLE_SCOPES constant with all 20 supported X OAuth2 scopes - Add DEFAULT_SCOPE constant set to "tweet.read users.read" - Add authorize_params method for scope handling and validation - Add comprehensive test coverage for scope functionality - Include all scopes from official X API documentation: - users.email for email access - media.write for media upload - Ensure backward compatibility with existing implementations --- lib/omniauth/strategies/twitter2.rb | 36 +++++++++++++++++++++++++++ test/omniauth/test_twitter2.rb | 38 +++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/lib/omniauth/strategies/twitter2.rb b/lib/omniauth/strategies/twitter2.rb index 58eb97b..c4f0599 100644 --- a/lib/omniauth/strategies/twitter2.rb +++ b/lib/omniauth/strategies/twitter2.rb @@ -6,6 +6,23 @@ module OmniAuth module Strategies class Twitter2 < OmniAuth::Strategies::OAuth2 # :nodoc: + # Available OAuth 2.0 scopes for X (Twitter) API v2 + # Reference: https://developer.x.com/en/docs/authentication/oauth-2-0/authorization-code + AVAILABLE_SCOPES = %w[ + tweet.read tweet.write tweet.moderate.write + users.email users.read + follows.read follows.write + offline.access space.read + mute.read mute.write + like.read like.write + list.read list.write + block.read block.write + bookmark.read bookmark.write + media.write + ].freeze + + DEFAULT_SCOPE = "tweet.read users.read" + option :name, "twitter2" # https://docs.x.com/fundamentals/authentication/oauth-2-0/overview option :client_options, { @@ -14,6 +31,9 @@ class Twitter2 < OmniAuth::Strategies::OAuth2 # :nodoc: authorize_url: "https://x.com/i/oauth2/authorize" } option :pkce, true + option :authorize_params, { + scope: DEFAULT_SCOPE + } uid { raw_info["data"]["id"] } @@ -47,6 +67,22 @@ def raw_info # https://github.com/zquestz/omniauth-google-oauth2/blob/475efe41ecfcf04b63921bd723ccf6fad429d1b1/lib/omniauth/strategies/google_oauth2.rb#L105 # https://github.com/simi/omniauth-facebook/blob/e1e572db2e9464871c98148621df1bbbe1e9f9c3/lib/omniauth/strategies/facebook.rb#L88 # https://github.com/omniauth/omniauth-oauth2/commit/85fdbe117c2a4400d001a6368cc359d88f40abc7 + def authorize_params + super.tap do |params| + # Ensure scope is included in authorize params + params[:scope] ||= options[:authorize_params][:scope] || DEFAULT_SCOPE + + # Validate scopes if provided + if params[:scope] + requested_scopes = params[:scope].split + invalid_scopes = requested_scopes - AVAILABLE_SCOPES + if !invalid_scopes.empty? && defined?(Rails) + Rails.logger.warn "Invalid Twitter OAuth2 scopes requested: #{invalid_scopes.join(", ")}" + end + end + end + end + def callback_url options[:callback_url] || (full_host + script_name + callback_path) end diff --git a/test/omniauth/test_twitter2.rb b/test/omniauth/test_twitter2.rb index d7de89e..b8a4ea5 100644 --- a/test/omniauth/test_twitter2.rb +++ b/test/omniauth/test_twitter2.rb @@ -53,4 +53,42 @@ def test_it_has_uid assert_equal "108252390", subject.uid end end + + def test_it_has_available_scopes_constant + assert_equal 20, OmniAuth::Strategies::Twitter2::AVAILABLE_SCOPES.length + assert_includes OmniAuth::Strategies::Twitter2::AVAILABLE_SCOPES, "tweet.read" + assert_includes OmniAuth::Strategies::Twitter2::AVAILABLE_SCOPES, "users.read" + assert_includes OmniAuth::Strategies::Twitter2::AVAILABLE_SCOPES, "users.email" + assert_includes OmniAuth::Strategies::Twitter2::AVAILABLE_SCOPES, "media.write" + assert_includes OmniAuth::Strategies::Twitter2::AVAILABLE_SCOPES, "offline.access" + end + + def test_it_has_default_scope + assert_equal "tweet.read users.read", OmniAuth::Strategies::Twitter2::DEFAULT_SCOPE + end + + def test_it_has_default_authorize_params + subject = strategy + assert_equal "tweet.read users.read", subject.options.authorize_params[:scope] + end + + def test_authorize_params_includes_scope + subject = strategy + params = subject.authorize_params + assert_equal "tweet.read users.read", params[:scope] + end + + def test_authorize_params_with_custom_scope + subject = strategy(authorize_params: { scope: "tweet.read tweet.write users.read" }) + params = subject.authorize_params + assert_equal "tweet.read tweet.write users.read", params[:scope] + end + + def test_authorize_params_validates_scopes + # This test ensures that invalid scopes are handled gracefully + subject = strategy(authorize_params: { scope: "invalid.scope tweet.read" }) + params = subject.authorize_params + # The method should still return params even with invalid scopes (just warn) + assert_equal "invalid.scope tweet.read", params[:scope] + end end From 0b6db2515c7417c60f85de1e43db55b1f2d0cebf Mon Sep 17 00:00:00 2001 From: HolyGrail Date: Sun, 3 Aug 2025 01:12:19 +0900 Subject: [PATCH 2/2] docs: add OAuth2 scope documentation and changelog - Document all 20 available OAuth2 scopes in README - Add usage examples for custom scope configuration - Update CHANGELOG with new features - Include descriptions for users.email and media.write scopes --- CHANGELOG.md | 10 ++++++++++ README.md | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b0eb54..80cbebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ ## [Unreleased] +### Added +- Added `AVAILABLE_SCOPES` constant with all supported X (Twitter) OAuth 2.0 scopes +- Added `DEFAULT_SCOPE` constant set to "tweet.read users.read" +- Added default `authorize_params` with scope configuration +- Added `authorize_params` method to handle scope validation and defaults +- Added comprehensive documentation for available scopes in README + +### Changed +- Scope parameter is now explicitly handled with defaults and validation + ## [1.0.0] - 2025-08-03 - Update api endpoints from `twitter.com` to `x.com` ([#7](https://github.com/unasuke/omniauth-twitter2/pull/7)) diff --git a/README.md b/README.md index 825812f..9f8c1d5 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,46 @@ $ gem install omniauth-twitter2 ```ruby # config/initializers/omniauth.rb Rails.application.config.middleware.use OmniAuth::Builder do - provider :twitter2, ENV["TWITTER_CLIENT_ID"], ENV["TWITTER_CLIENT_SECRET"], callback_path: '/auth/twitter2/callback', scope: "tweet.read users.read" + provider :twitter2, ENV["TWITTER_CLIENT_ID"], ENV["TWITTER_CLIENT_SECRET"], + callback_path: '/auth/twitter2/callback', + scope: "tweet.read users.read" # Default scope +end +``` + +### Available Scopes + +The following OAuth 2.0 scopes are available for X (Twitter) API v2: + +- `tweet.read` - All the Tweets you can view, including Tweets from protected accounts. +- `tweet.write` - Tweet and Retweet for you. +- `tweet.moderate.write` - Hide and unhide replies to your Tweets. +- `users.email` - Email from an authenticated user. +- `users.read` - Any account you can view, including protected accounts. +- `follows.read` - People who follow you and people who you follow. +- `follows.write` - Follow and unfollow people for you. +- `offline.access` - Stay connected to your account until you revoke access. +- `space.read` - All the Spaces you can view. +- `mute.read` - Accounts you've muted. +- `mute.write` - Mute and unmute accounts for you. +- `like.read` - Tweets you've liked and likes you can view. +- `like.write` - Like and un-like Tweets for you. +- `list.read` - Lists, list members, and list followers of lists you've created or are a member of, including private lists. +- `list.write` - Create and manage Lists for you. +- `block.read` - Accounts you've blocked. +- `block.write` - Block and unblock accounts for you. +- `bookmark.read` - Get Bookmarked Tweets from an authenticated user. +- `bookmark.write` - Bookmark and remove Bookmarks from Tweets. +- `media.write` - Upload media. + +Default scope is `"tweet.read users.read"` if not specified. + +You can customize the scope like this: + +```ruby +Rails.application.config.middleware.use OmniAuth::Builder do + provider :twitter2, ENV["TWITTER_CLIENT_ID"], ENV["TWITTER_CLIENT_SECRET"], + callback_path: '/auth/twitter2/callback', + scope: "tweet.read tweet.write users.read offline.access" end ```