Skip to content

Conversation

@m2rads
Copy link

@m2rads m2rads commented Jan 12, 2026

Hello all, this PR closes issue #20.

Users can:

  • Connect their X account from their profile page
  • Disconnect their X account
  • Login with X only if they've already connected their account

Changes:

  • Added omniauth, omniauth-twitter2, and omniauth-rails_csrf_protection gems
  • Created migration for OAuth fields (twitter_uid, twitter_oauth_token, twitter_oauth_refresh_token, etc.)
  • Implemented OAuth callback controller with connect/disconnect/login flows
  • Added "Connect X Account" button to profile page
  • Added "Login with X" button to login page
  • Added uniqueness validation for twitter_uid
  • Added controller tests for OAuth flows

Testing:

  • Tested connect/disconnect flows
  • Tested login with connected account
  • Tested error handling for duplicate account linking

AI Disclosure:

  • Model: Claude Opus 4.5 (via Cursor)
  • Used for: Codebase exploration, understanding existing authentication patterns, generating test structure, and PR description.
  • Each file that was modified by AI is manually reviewed by me.

Test Execution result before code changes:
Screenshot 2026-01-11 at 6 20 14 PM

Test Execution result after code changes plus the new tests added for this feature:
image

Demo:
https://github.com/user-attachments/assets/4264f624-f82a-454f-8a0c-2c03a64a62e3

Copy link
Author

@m2rads m2rads left a comment

Choose a reason for hiding this comment

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

Hi @dvassallo! Here is my self review. Please let me know if any changes or further explanation required. Thanks!

Comment on lines +83 to +85
gem "omniauth"
gem "omniauth-twitter2"
gem "omniauth-rails_csrf_protection"
Copy link
Author

Choose a reason for hiding this comment

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

These three gems are recommended on X documentation page here.

omniauth-twitter2 is dependent on omniauth.
omniauth-rails_csrf_protection adds CSRF protection to omniauth.

Comment on lines +174 to +179

<% unless @user.twitter_connected_at.present? %>
<form id="x-oauth-form" action="/auth/twitter2" method="post" style="display: none;">
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
</form>
<% end %>
Copy link
Author

Choose a reason for hiding this comment

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

This is the form where it initiates the X account linking by calling the auth/twitter2 endpoint.

Comment on lines +1 to +12
class AddTwitterOAuthToUsers < ActiveRecord::Migration[7.2]
def change
add_column :users, :twitter_uid, :string
add_column :users, :twitter_oauth_token, :text
add_column :users, :twitter_oauth_refresh_token, :text
add_column :users, :twitter_screen_name, :string
add_column :users, :twitter_profile_image, :string
add_column :users, :twitter_connected_at, :datetime

add_index :users, :twitter_uid, unique: true
end
end
Copy link
Author

Choose a reason for hiding this comment

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

Adds OAuth db fields to User table. This will be used for refreshing the session and logging in during X OAuth flow.

Comment on lines +1 to +6
Rails.application.config.middleware.use OmniAuth::Builder do
provider :twitter2,
ENV["X_CLIENT_ID"],
ENV["X_CLIENT_SECRET"],
scope: "tweet.read users.read offline.access"
end
Copy link
Author

Choose a reason for hiding this comment

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

Configures OmniAuth middleware with X client ID/secret and required scopes.

Comment on lines +75 to +76
get "/auth/failure", to: "users/omniauth_callbacks#failure"
delete "/auth/twitter/disconnect", to: "users/omniauth_callbacks#disconnect"
Copy link
Author

Choose a reason for hiding this comment

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

These two endpoints handle auth failures and account disconnection.


validates_presence_of :email_address, if: :person?
normalizes :email_address, with: ->(email_address) { email_address.downcase }
validates :twitter_uid, uniqueness: true, allow_nil: true
Copy link
Author

Choose a reason for hiding this comment

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

Prevents one X account from being linked to multiple users.

Comment on lines +219 to +222
def twitter_connected?
twitter_uid.present? && twitter_connected_at.present?
end

Copy link
Author

Choose a reason for hiding this comment

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

Helper to check if account is linked. This is only used in the test to make sure that the X account is linked properly.

Comment on lines +14 to +58
def mock_auth_hash(uid: "123456789", nickname: "testuser")
OmniAuth::AuthHash.new({
"provider" => "twitter2",
"uid" => uid,
"info" => {
"nickname" => nickname,
"image" => "https://pbs.twimg.com/profile_images/test.jpg"
},
"credentials" => {
"token" => "test_token",
"refresh_token" => "test_refresh"
}
})
end

test "twitter callback for login signs in user with linked X account" do
user = users(:david)
user.update!(twitter_uid: "123456789", twitter_oauth_token: "token")

OmniAuth.config.mock_auth[:twitter2] = mock_auth_hash

post "/auth/twitter2/login"
assert_response :success

get "/auth/twitter2/callback"

assert_redirected_to root_path
assert_equal "Signed in with X successfully!", flash[:notice]
end

test "twitter callback for login rejects user without linked X account" do
OmniAuth.config.mock_auth[:twitter2] = mock_auth_hash(uid: "nonexistent")

post "/auth/twitter2/login"
get "/auth/twitter2/callback"

assert_redirected_to new_session_path
assert_equal "No account found with this X account. Please sign in with email first and connect your X account.", flash[:alert]
end

test "twitter callback for connect redirects when not signed in" do
OmniAuth.config.mock_auth[:twitter2] = mock_auth_hash

get "/auth/twitter2/callback"

Copy link
Author

Choose a reason for hiding this comment

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

Tests login flow (success and failure cases)

Comment on lines +62 to +106

test "disconnect clears X account data" do
sign_in :david
user = users(:david)
user.update!(
twitter_uid: "123456789",
twitter_oauth_token: "token",
twitter_oauth_refresh_token: "refresh",
twitter_screen_name: "testuser",
twitter_profile_image: "https://example.com/image.jpg",
twitter_connected_at: Time.current
)

delete "/auth/twitter/disconnect"

assert_redirected_to user_profile_path("me")
assert_equal "X account disconnected successfully!", flash[:notice]

user.reload
assert_nil user.twitter_uid
assert_nil user.twitter_oauth_token
assert_nil user.twitter_screen_name
assert_nil user.twitter_connected_at
end

test "disconnect requires authentication" do
delete "/auth/twitter/disconnect"

assert_redirected_to new_session_path
end

test "initiate_login renders auto-submit form" do
post "/auth/twitter2/login"

assert_response :success
assert_includes response.body, 'action="/auth/twitter2"'
end

test "failure redirects with error message" do
get "/auth/failure", params: { message: "access_denied" }

assert_redirected_to user_profile_path("me")
assert_equal "X authentication failed: access_denied", flash[:alert]
end

Copy link
Author

Choose a reason for hiding this comment

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

Tests disconnect functionality.

Comment on lines +107 to +137
test "user twitter_uid must be unique" do
users(:david).update!(twitter_uid: "123456789")

user = users(:jason)
user.twitter_uid = "123456789"

assert_not user.valid?
assert_includes user.errors[:twitter_uid], "has already been taken"
end

test "twitter_connected? returns true when uid and connected_at present" do
user = users(:david)
user.update!(twitter_uid: "123", twitter_connected_at: Time.current)

assert user.twitter_connected?
end

test "twitter_connected? returns false when uid missing" do
user = users(:david)
user.update!(twitter_uid: nil, twitter_connected_at: Time.current)

assert_not user.twitter_connected?
end

test "twitter_connected? returns false when connected_at missing" do
user = users(:david)
user.update!(twitter_uid: "123", twitter_connected_at: nil)

assert_not user.twitter_connected?
end
end
Copy link
Author

Choose a reason for hiding this comment

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

Test proper X account connection and account uniqueness.

@m2rads m2rads changed the title Feat/x oauth Feature: Linking X/Twitter Account in the Profile and Logging in via X oauth Jan 12, 2026
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.

1 participant