Skip to content

Conversation

@sferik
Copy link
Owner

@sferik sferik commented Dec 26, 2025

No description provided.

Implement OAuth2Authenticator class that supports:
- Token storage (access_token, refresh_token, expires_at)
- Automatic token expiration checking via token_expired?
- Token refresh via refresh_token! method
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds OAuth 2.0 authentication support with automatic token refresh capability to the X API client. The implementation follows the existing authenticator pattern and integrates seamlessly with the client's credential precedence system.

Key changes:

  • New OAuth2Authenticator class handling bearer token auth and automatic token refresh
  • Extended Client to accept OAuth 2.0 credentials (client_id, client_secret, refresh_token)
  • Comprehensive test coverage for OAuth 2.0 flows including token expiration and refresh

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
lib/x/oauth2_authenticator.rb New authenticator class implementing OAuth 2.0 with token refresh using client credentials
lib/x/client_credentials.rb Extended credential management to support OAuth 2.0 fields and authenticator selection
lib/x/client.rb Updated Client initialization to accept OAuth 2.0 credentials
sig/x.rbs Added type signatures for OAuth2Authenticator and updated Client/ClientCredentials signatures
test/x/oauth2_authenticator_test.rb Comprehensive test suite covering initialization, header generation, expiration checks, and token refresh
test/x/client_initialization_test.rb Reorganized tests into focused test classes and added OAuth 2.0 initialization tests
test/test_helper.rb Added OAuth 2.0 test constants and credential helper method

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Change TOKEN_URL and TOKEN_HOST from api.x.com to api.twitter.com
to match the rest of the client's default API host configuration.
Repository owner deleted a comment from chatgpt-codex-connector bot Dec 26, 2025
Copy link
Collaborator

@GCorbel GCorbel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a few questions ATM, but I give a closer look and try it tomorrow.

@GCorbel
Copy link
Collaborator

GCorbel commented Dec 30, 2025

You can remove "* OAuth 2.0 Bearer Token", in "## Features", in the README and add a line in CHANGELOG.

@sferik
Copy link
Owner Author

sferik commented Dec 30, 2025

You can remove "* OAuth 2.0 Bearer Token", in "## Features", in the README and add a line in CHANGELOG.

Will do.

Any other feedback or is this good to merge?

Copy link
Collaborator

@GCorbel GCorbel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried rapidly but without refresh token. Let me know if you want I do more tests.

This allows proxy and timeout settings to be configured for token
refresh requests, rather than using a hardcoded HTTP client.
Treats tokens as expired 30 seconds early to account for clock skew and
network latency, preventing requests with nearly-expired tokens.
@sferik
Copy link
Owner Author

sferik commented Jan 1, 2026

@GCorbel I believe I've addressed all your feedback. Would you mind testing the latest revision and giving it a final thumbs up before I merge and release it?

Copy link
Collaborator

@GCorbel GCorbel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created (mostly AI generated) to be able to generate a refresh token:

# ============================================================================
# X OAuth 2.0 Flow Example with PKCE
# ============================================================================
#
# This script demonstrates the complete OAuth 2.0 authorization flow with PKCE
# (Proof Key for Code Exchange) for the X API.
# Read https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code for details.
#
# USAGE:
#   1. Add http://localhost:8000/callback in "Callback URI / Redirect URL" in the app authentication settings
#
#   2. Set your OAuth 2.0 credentials:
#      export CLIENT_ID="your_client_id"
#      export CLIENT_SECRET="your_client_secret"
#
#   3. Run this script:
#      ruby oauth2_flow.rb
#
#   4. Open http://localhost:8000 in your browser
#
#   5. You'll be redirected to X to authorize the app
#
#   6. After authorization, you'll see your access token
#
# ============================================================================

require "bundler/inline"

gemfile do
  source "https://rubygems.org"
  gem "sinatra"
  gem "rackup"
  gem "puma"
end

require "net/http"
require "json"
require "securerandom"
require "digest"
require "base64"
require "uri"

# ----------------------------------------------------------------------------
# Configuration
# ----------------------------------------------------------------------------

CLIENT_ID = ENV['CLIENT_ID']
CLIENT_SECRET = ENV['CLIENT_SECRET']
REDIRECT_URI = "http://localhost:8000/callback"
SCOPE = "tweet.read users.read follows.read offline.access"

# X OAuth endpoints
AUTH_URL = "https://x.com/i/oauth2/authorize"
TOKEN_URL = "https://api.x.com/2/oauth2/token"

# ----------------------------------------------------------------------------
# OAuth State Storage
# ----------------------------------------------------------------------------

# In-memory storage for OAuth state
# NOTE: For production apps, use sessions or a database
$oauth_state = {}

# ----------------------------------------------------------------------------
# Sinatra Web Server Configuration
# ----------------------------------------------------------------------------

set :port, 8000
set :bind, "0.0.0.0"

# ----------------------------------------------------------------------------
# Route 1: Start OAuth Flow
# ----------------------------------------------------------------------------

get "/" do
  # Step 1: Generate PKCE values for security
  # - Code verifier: random 96-byte string (128 chars when base64-encoded)
  # - Code challenge: SHA256 hash of verifier, base64-encoded
  code_verifier = SecureRandom.urlsafe_base64(96)
  code_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)
  state = SecureRandom.hex(16)

  # Step 2: Store the verifier to use later in callback
  $oauth_state[state] = code_verifier

  # Step 3: Build authorization URL with all required parameters
  auth_params = {
    response_type: "code",
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URI,
    scope: SCOPE,
    state: state,
    code_challenge: code_challenge,
    code_challenge_method: "S256"
  }

  auth_url = "#{AUTH_URL}?#{URI.encode_www_form(auth_params)}"

  # Step 4: Redirect user to X for authorization
  redirect auth_url
end

# ----------------------------------------------------------------------------
# Route 2: Handle OAuth Callback
# ----------------------------------------------------------------------------

get "/callback" do
  # Step 1: Extract callback parameters
  state = params[:state]
  code = params[:code]
  error = params[:error]

  # Step 2: Check for errors from X
  return "❌ OAuth Error: #{error}" if error

  # Step 3: Verify state and retrieve code verifier
  verifier = $oauth_state.delete(state)
  return "❌ Error: Invalid state or session expired!" if verifier.nil?

  # Step 4: Exchange authorization code for access token
  token_response = exchange_code_for_token(code, verifier)

  # Step 5: Display the token response
  content_type :json
  JSON.pretty_generate(token_response)
end

# ----------------------------------------------------------------------------
# Helper: Exchange Authorization Code for Access Token
# ----------------------------------------------------------------------------

def exchange_code_for_token(code, verifier)
  # Prepare HTTP request to token endpoint
  uri = URI(TOKEN_URL)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  request = Net::HTTP::Post.new(uri)
  request["Content-Type"] = "application/x-www-form-urlencoded"

  # Add Basic Authentication header (required for confidential clients)
  credentials = Base64.strict_encode64("#{CLIENT_ID}:#{CLIENT_SECRET}")
  request["Authorization"] = "Basic #{credentials}"

  # Add request body with token exchange parameters
  request.body = URI.encode_www_form({
    code: code,
    grant_type: "authorization_code",
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URI,
    code_verifier: verifier
  })

  # Make the request and parse response
  response = http.request(request)
  JSON.parse(response.body)
end

# ----------------------------------------------------------------------------
# Start the Server
# ----------------------------------------------------------------------------

Sinatra::Application.run!

Feel free to add it in examples.

@sferik sferik merged commit 8953675 into main Jan 5, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants