-
-
Notifications
You must be signed in to change notification settings - Fork 23
Add OAuth 2.0 authentication with token refresh support #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
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
There was a problem hiding this 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".
There was a problem hiding this 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
OAuth2Authenticatorclass handling bearer token auth and automatic token refresh - Extended
Clientto 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.
GCorbel
left a comment
There was a problem hiding this 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.
|
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? |
GCorbel
left a comment
There was a problem hiding this 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.
|
@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? |
GCorbel
left a comment
There was a problem hiding this 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.
No description provided.