chore: rotate oauth password if unconfirmed (#13878)

When a user signs up with an email they don't own and sets a password,
that password remains valid even after the real owner later signs in via
OAuth. This means the original registrant — who never proved ownership
of the email — retains working credentials on the account. This change
closes that gap by rotating the password to a random value whenever an
unconfirmed user completes an OAuth sign-in.

The check (`oauth_user_needs_password_reset?`) is evaluated before
`skip_confirmation!` runs, since confirmation would flip `confirmed_at`
and mask the condition. If the user was unconfirmed, the stored password
is replaced with a secure random string that satisfies the password
policy. This applies to both the web and mobile OAuth callback paths, as
well as the sign-up path where the password is rotated before the reset
token is generated.

Users who lose access to password-based login as a side effect can
recover through the standard "Forgot password" flow at any time. Since
they've already proven email ownership via OAuth, this is a low-friction
recovery path
This commit is contained in:
Shivam Mishra
2026-04-02 11:26:29 +05:30
committed by GitHub
parent 7b09b033ef
commit 211fb1102d
2 changed files with 34 additions and 0 deletions

View File

@@ -164,5 +164,21 @@ RSpec.describe 'DeviseOverrides::OmniauthCallbacksController', type: :request do
expect(response).to have_http_status(:ok)
end
end
it 'resets password for an unconfirmed persisted user on OAuth login' do
with_modified_env FRONTEND_URL: 'http://www.example.com' do
user = create(:user, email: 'unconfirmed-oauth@example.com', skip_confirmation: false)
original_password_digest = user.encrypted_password
set_omniauth_config('unconfirmed-oauth@example.com')
get '/omniauth/google_oauth2/callback'
expect(response).to redirect_to('http://www.example.com/auth/google_oauth2/callback')
follow_redirect!
user.reload
expect(user).to be_confirmed
expect(user.encrypted_password).not_to eq(original_password_digest)
end
end
end
end