Skip to content

Commit 9c2399d

Browse files
authored
Merge pull request code-dot-org#24870 from code-dot-org/multi-auth-reverse-takeover
Handle linking credential already claimed by another account
2 parents a193090 + 8ba8a67 commit 9c2399d

File tree

3 files changed

+139
-1
lines changed

3 files changed

+139
-1
lines changed

dashboard/app/controllers/omniauth_callbacks_controller.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,31 @@ def connect_provider
2727
provider = auth_hash.provider.to_s
2828
return head(:bad_request) unless AuthenticationOption::OAUTH_CREDENTIAL_TYPES.include? provider
2929

30+
existing_credential_holder = User.find_by_credential type: provider, id: auth_hash.uid
31+
32+
# Credential is already held by the current user
33+
# Notify of no-op.
34+
if existing_credential_holder&.==(current_user)
35+
flash.notice = I18n.t('auth.already_linked', provider: I18n.t("auth.#{provider}"))
36+
return redirect_to edit_user_registration_path
37+
end
38+
39+
# Credential is already held by another user with activity
40+
# Display an error explaining that the credential is already in use.
41+
if existing_credential_holder&.has_activity?
42+
flash.alert = I18n.t('auth.already_in_use', provider: I18n.t("auth.#{provider}"))
43+
return redirect_to edit_user_registration_path
44+
end
45+
46+
# Credential is already held by an unused account.
47+
# Take over the unused account.
48+
if existing_credential_holder
49+
move_sections_and_destroy_source_user \
50+
source_user: existing_credential_holder,
51+
destination_user: current_user,
52+
takeover_type: 'connect_provider'
53+
end
54+
3055
# TODO: some of this won't work right for non-Google providers, because info comes in differently
3156
new_data = nil
3257
if auth_hash.credentials && (auth_hash.credentials.token || auth_hash.credentials.expires_at || auth_hash.credentials.refresh_token)

dashboard/config/locales/en.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ en:
117117
not_linked: "Sorry, your Code.org account is not linked to your %{provider}. You'll have to sign in by typing your password below."
118118
use_clever: "Sorry, your Code.org account is not linked to your %{provider}. Please sign in through your Clever Portal instead."
119119
unable_to_connect_provider: "Unable to connect your Code.org account to your %{provider}. Please try again."
120+
already_in_use: "Your %{provider} is already connected to another Code.org account."
121+
already_linked: "Your %{provider} is already linked to your Code.org account."
120122
signup_form:
121123
student_count: '%{count} students have already signed up.'
122124
teacher_count: '%{count} teachers have already signed up.'

dashboard/test/controllers/omniauth_callbacks_controller_test.rb

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,106 @@ class OmniauthCallbacksControllerTest < ActionController::TestCase
822822
end
823823
end
824824

825+
test "connect_provider: Performs takeover of an account with matching credential that has no activity" do
826+
user = create :user, :multi_auth_migrated
827+
828+
# Given there exists another user
829+
# having credential X
830+
# and having no activity
831+
other_user = create :user, :multi_auth_migrated
832+
credential = create :google_authentication_option, user: other_user
833+
refute other_user.has_activity?
834+
835+
# When I attempt to add credential X
836+
auth = link_credential user,
837+
type: credential.credential_type,
838+
id: credential.authentication_id
839+
840+
# Then I should successfully add credential X
841+
user.reload
842+
assert_redirected_to 'http://test.host/users/edit'
843+
assert_auth_option(user, auth)
844+
845+
# And the other user should be destroyed
846+
other_user.reload
847+
assert other_user.deleted?
848+
end
849+
850+
test "connect_provider: Successful takeover also enrolls in replaced user's sections" do
851+
user = create :user, :multi_auth_migrated
852+
853+
# Given there exists another user
854+
# having credential X
855+
# and having no activity
856+
# and enrolled in section Y
857+
other_user = create :user, :multi_auth_migrated
858+
credential = create :google_authentication_option, user: other_user
859+
refute other_user.has_activity?
860+
section = create :section
861+
section.students << other_user
862+
863+
# When I add credential X
864+
link_credential user,
865+
type: credential.credential_type,
866+
id: credential.authentication_id
867+
868+
# Then I should be enrolled in section Y instead of the other user
869+
section.reload
870+
assert_includes section.students, user
871+
refute_includes section.students, other_user
872+
end
873+
874+
test "connect_provider: Refuses to link credential if there is an account with matching credential that has activity" do
875+
user = create :user, :multi_auth_migrated
876+
877+
# Given there exists another user
878+
# having credential X
879+
# and having activity
880+
other_user = create :user, :multi_auth_migrated
881+
credential = create :google_authentication_option, user: other_user
882+
create :user_level, user: other_user, best_result: ActivityConstants::MINIMUM_PASS_RESULT
883+
assert other_user.has_activity?
884+
885+
# When I attempt to add credential X
886+
link_credential user,
887+
type: credential.credential_type,
888+
id: credential.authentication_id
889+
890+
# Then the other user should not be destroyed
891+
other_user.reload
892+
refute other_user.deleted?
893+
894+
# And I should fail to add credential X
895+
user.reload
896+
assert_empty user.authentication_options
897+
898+
# And receive a helpful error message about the credential already being in use.
899+
assert_redirected_to 'http://test.host/users/edit'
900+
expected_error = I18n.t('auth.already_in_use', provider: I18n.t("auth.google_oauth2"))
901+
assert_equal expected_error, flash.alert
902+
end
903+
904+
test "connect_provider: Presents no-op message if the provided credentials are already linked to user's account" do
905+
# Given the current user already has credential X
906+
user = create :user, :multi_auth_migrated
907+
credential = create :google_authentication_option, user: user
908+
assert_equal 1, user.authentication_options.count
909+
910+
# When I attempt to add credential X
911+
link_credential user,
912+
type: credential.credential_type,
913+
id: credential.authentication_id
914+
915+
# Then I should have the same authentication options
916+
user.reload
917+
assert_equal 1, user.authentication_options.count
918+
919+
# And receive a friendly notice about already having the credential
920+
assert_redirected_to 'http://test.host/users/edit'
921+
expected_notice = I18n.t('auth.already_linked', provider: I18n.t("auth.google_oauth2"))
922+
assert_equal expected_notice, flash.notice
923+
end
924+
825925
private
826926

827927
def set_oauth_takeover_session_variables(provider, user)
@@ -831,6 +931,17 @@ def set_oauth_takeover_session_variables(provider, user)
831931
@request.session[ACCT_TAKEOVER_OAUTH_TOKEN] = '54321'
832932
end
833933

934+
# Try to link a credential to the provided user
935+
# @return [OmniAuth::AuthHash] the auth hash, useful for validating
936+
# linked credentials with assert_auth_option
937+
def link_credential(user, type:, id:)
938+
auth = generate_auth_user_hash(provider: type, uid: id)
939+
@request.env['omniauth.auth'] = auth
940+
setup_should_connect_provider(user, 2.days.from_now)
941+
get :google_oauth2
942+
auth
943+
end
944+
834945
def generate_auth_user_hash(args)
835946
OmniAuth::AuthHash.new(
836947
uid: args[:uid] || '1111',
@@ -863,7 +974,7 @@ def assert_auth_option(user, oauth_hash)
863974
user: user,
864975
hashed_email: User.hash_email(oauth_hash.info.email),
865976
credential_type: oauth_hash.provider,
866-
authentication_id: user.uid,
977+
authentication_id: oauth_hash.uid,
867978
data: {
868979
oauth_token: oauth_hash.credentials.token,
869980
oauth_token_expiration: oauth_hash.credentials.expires_at,

0 commit comments

Comments
 (0)