From bc73c376be2348b585e86e6ac5e7cdf22186716c Mon Sep 17 00:00:00 2001 From: mkovalua Date: Thu, 26 Mar 2026 15:41:38 +0200 Subject: [PATCH 1/5] If a user has an existing ORCID ID, and then associates a new ORCID ID, the existing ORCID ID should be removed/overwritten --- api/users/views.py | 6 ++++-- framework/auth/views.py | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/api/users/views.py b/api/users/views.py index b920798be86..4a34801e9e2 100644 --- a/api/users/views.py +++ b/api/users/views.py @@ -755,7 +755,10 @@ def post(self, request, *args, **kwargs): # 1. update user oauth, with pending status external_identity[external_id_provider][external_id] = 'LINK' if external_id_provider in user.external_identity: - user.external_identity[external_id_provider].update(external_identity[external_id_provider]) + if external_id_provider == 'orcid': + user.external_identity[external_id_provider] = external_identity[external_id_provider] + else: + user.external_identity[external_id_provider].update(external_identity[external_id_provider]) else: user.external_identity.update(external_identity) if not user.accepted_terms_of_service and accepted_terms_of_service: @@ -1153,7 +1156,6 @@ class ConfirmEmailView(generics.CreateAPIView): def _process_external_identity(self, user, external_identity, service_url): """Handle all external_identity logic, including task enqueueing and url updates.""" - provider = next(iter(external_identity)) if provider not in user.external_identity: raise ValidationError('External-ID provider mismatch.') diff --git a/framework/auth/views.py b/framework/auth/views.py index aefb1b4f0e8..0e72df882c7 100644 --- a/framework/auth/views.py +++ b/framework/auth/views.py @@ -1097,7 +1097,10 @@ def external_login_email_post(): # 1. update user oauth, with pending status external_identity[external_id_provider][external_id] = 'LINK' if external_id_provider in user.external_identity: - user.external_identity[external_id_provider].update(external_identity[external_id_provider]) + if external_id_provider == 'orcid': + user.external_identity[external_id_provider] = external_identity[external_id_provider] + else: + user.external_identity[external_id_provider].update(external_identity[external_id_provider]) else: user.external_identity.update(external_identity) if not user.accepted_terms_of_service and form.accepted_terms_of_service.data: From 41ad2daaec47fb15e2f1490e6adbb533392b1a36 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Fri, 27 Mar 2026 15:00:09 +0200 Subject: [PATCH 2/5] add test to check if orcid is overwritten --- .../users/views/test_user_external_login.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/api_tests/users/views/test_user_external_login.py b/api_tests/users/views/test_user_external_login.py index 6f436f5d422..9278ac984ae 100644 --- a/api_tests/users/views/test_user_external_login.py +++ b/api_tests/users/views/test_user_external_login.py @@ -87,3 +87,21 @@ def test_existing_user(self, app, payload, url, user_one, session_data, csrf_tok assert res.json == {'external_id_provider': 'orcid', 'auth_user_fullname': 'external login'} user_one.reload() assert user_one.username in user_one.unconfirmed_emails + + def test_existing_user_orcid_overwrites(self, app, payload, url, user_one, session_data, csrf_token): + user_one.external_identity = { + 'orcid': { + '0000-0000-0000-0000': 'LINK', + } + } + user_one.save() + app.set_cookie(CSRF_COOKIE_NAME, csrf_token) + app.set_cookie(settings.COOKIE_NAME, str(session_data)) + assert '0000-0000-0000-0000' in user_one.external_identity['orcid'] + payload['data']['attributes']['email'] = user_one.username + with capture_notifications(): + res = app.post_json_api(url, payload, headers={'X-CSRFToken': csrf_token}) + assert res.status_code == 200 + user_one.reload() + assert '0000-0000-0000-0000' not in user_one.external_identity['orcid'] + assert user_one.external_identity['orcid'] == {'1234-1234-1234-1234': 'LINK'} From 58aa808971b35f0bccb6cb949de3283caf460e94 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Mon, 30 Mar 2026 22:40:56 +0300 Subject: [PATCH 3/5] update literal from 'orcid' to 'ORCID' --- api/users/views.py | 3 ++- api_tests/users/views/test_user_external_login.py | 8 ++++---- framework/auth/views.py | 3 ++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/api/users/views.py b/api/users/views.py index 4a34801e9e2..c349a397239 100644 --- a/api/users/views.py +++ b/api/users/views.py @@ -755,7 +755,8 @@ def post(self, request, *args, **kwargs): # 1. update user oauth, with pending status external_identity[external_id_provider][external_id] = 'LINK' if external_id_provider in user.external_identity: - if external_id_provider == 'orcid': + # v2 looks to be used for auth but add orcid external identity rewrite updates for v1 as well + if external_id_provider == settings.EXTERNAL_IDENTITY_PROFILE.get('OrcidProfile'): user.external_identity[external_id_provider] = external_identity[external_id_provider] else: user.external_identity[external_id_provider].update(external_identity[external_id_provider]) diff --git a/api_tests/users/views/test_user_external_login.py b/api_tests/users/views/test_user_external_login.py index 9278ac984ae..d53dc402506 100644 --- a/api_tests/users/views/test_user_external_login.py +++ b/api_tests/users/views/test_user_external_login.py @@ -90,18 +90,18 @@ def test_existing_user(self, app, payload, url, user_one, session_data, csrf_tok def test_existing_user_orcid_overwrites(self, app, payload, url, user_one, session_data, csrf_token): user_one.external_identity = { - 'orcid': { + 'ORCID': { '0000-0000-0000-0000': 'LINK', } } user_one.save() app.set_cookie(CSRF_COOKIE_NAME, csrf_token) app.set_cookie(settings.COOKIE_NAME, str(session_data)) - assert '0000-0000-0000-0000' in user_one.external_identity['orcid'] + assert '0000-0000-0000-0000' in user_one.external_identity['ORCID'] payload['data']['attributes']['email'] = user_one.username with capture_notifications(): res = app.post_json_api(url, payload, headers={'X-CSRFToken': csrf_token}) assert res.status_code == 200 user_one.reload() - assert '0000-0000-0000-0000' not in user_one.external_identity['orcid'] - assert user_one.external_identity['orcid'] == {'1234-1234-1234-1234': 'LINK'} + assert '0000-0000-0000-0000' not in user_one.external_identity['ORCID'] + assert user_one.external_identity['ORCID'] == {'1234-1234-1234-1234': 'LINK'} diff --git a/framework/auth/views.py b/framework/auth/views.py index 0e72df882c7..140d6d78c43 100644 --- a/framework/auth/views.py +++ b/framework/auth/views.py @@ -1097,7 +1097,8 @@ def external_login_email_post(): # 1. update user oauth, with pending status external_identity[external_id_provider][external_id] = 'LINK' if external_id_provider in user.external_identity: - if external_id_provider == 'orcid': + # v2 looks to be used for auth but add orcid external identity rewrite updates for v1 as well + if external_id_provider == settings.EXTERNAL_IDENTITY_PROFILE.get('OrcidProfile'): user.external_identity[external_id_provider] = external_identity[external_id_provider] else: user.external_identity[external_id_provider].update(external_identity[external_id_provider]) From a30831ed72863f0abc7c1bcd8e91ac172ae0c152 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Mon, 30 Mar 2026 23:44:28 +0300 Subject: [PATCH 4/5] update tests --- api_tests/users/views/test_user_external_login.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api_tests/users/views/test_user_external_login.py b/api_tests/users/views/test_user_external_login.py index d53dc402506..fca05aadbc8 100644 --- a/api_tests/users/views/test_user_external_login.py +++ b/api_tests/users/views/test_user_external_login.py @@ -48,7 +48,7 @@ def csrf_token(self): @pytest.fixture() def session_data(self): session = SessionStore() - session['auth_user_external_id_provider'] = 'orcid' + session['auth_user_external_id_provider'] = 'ORCID' session['auth_user_external_id'] = '1234-1234-1234-1234' session['auth_user_fullname'] = 'external login' session['auth_user_external_first_login'] = True @@ -62,7 +62,7 @@ def test_external_login(self, app, payload, url, session_data, csrf_token): with capture_notifications(): res = app.post_json_api(url, payload, headers={'X-CSRFToken': csrf_token}) assert res.status_code == 200 - assert res.json == {'external_id_provider': 'orcid', 'auth_user_fullname': 'external login'} + assert res.json == {'external_id_provider': 'ORCID', 'auth_user_fullname': 'external login'} assert not OSFUser.objects.get(username='freddie@mercury.com').is_confirmed def test_invalid_payload(self, app, url, session_data, csrf_token): @@ -84,7 +84,7 @@ def test_existing_user(self, app, payload, url, user_one, session_data, csrf_tok with capture_notifications(): res = app.post_json_api(url, payload, headers={'X-CSRFToken': csrf_token}) assert res.status_code == 200 - assert res.json == {'external_id_provider': 'orcid', 'auth_user_fullname': 'external login'} + assert res.json == {'external_id_provider': 'ORCID', 'auth_user_fullname': 'external login'} user_one.reload() assert user_one.username in user_one.unconfirmed_emails @@ -97,11 +97,11 @@ def test_existing_user_orcid_overwrites(self, app, payload, url, user_one, sessi user_one.save() app.set_cookie(CSRF_COOKIE_NAME, csrf_token) app.set_cookie(settings.COOKIE_NAME, str(session_data)) + assert user_one.external_identity['ORCID'] == {'0000-0000-0000-0000': 'LINK'} assert '0000-0000-0000-0000' in user_one.external_identity['ORCID'] payload['data']['attributes']['email'] = user_one.username with capture_notifications(): res = app.post_json_api(url, payload, headers={'X-CSRFToken': csrf_token}) assert res.status_code == 200 user_one.reload() - assert '0000-0000-0000-0000' not in user_one.external_identity['ORCID'] assert user_one.external_identity['ORCID'] == {'1234-1234-1234-1234': 'LINK'} From 7f09471ac97657c93c87862b80e7b9f08958d40e Mon Sep 17 00:00:00 2001 From: mkovalua Date: Wed, 1 Apr 2026 17:06:40 +0300 Subject: [PATCH 5/5] v1 looks to be used for orcid auth (give the hint with comment) --- api/users/views.py | 2 +- framework/auth/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/users/views.py b/api/users/views.py index c349a397239..b887ee7bdd6 100644 --- a/api/users/views.py +++ b/api/users/views.py @@ -755,7 +755,7 @@ def post(self, request, *args, **kwargs): # 1. update user oauth, with pending status external_identity[external_id_provider][external_id] = 'LINK' if external_id_provider in user.external_identity: - # v2 looks to be used for auth but add orcid external identity rewrite updates for v1 as well + # v1 looks to be used because of /confirm/external/ usage for auth but add orcid external identity rewrite updates for v2 as well if external_id_provider == settings.EXTERNAL_IDENTITY_PROFILE.get('OrcidProfile'): user.external_identity[external_id_provider] = external_identity[external_id_provider] else: diff --git a/framework/auth/views.py b/framework/auth/views.py index 140d6d78c43..c40605ca512 100644 --- a/framework/auth/views.py +++ b/framework/auth/views.py @@ -1097,7 +1097,7 @@ def external_login_email_post(): # 1. update user oauth, with pending status external_identity[external_id_provider][external_id] = 'LINK' if external_id_provider in user.external_identity: - # v2 looks to be used for auth but add orcid external identity rewrite updates for v1 as well + # v1 looks to be used because of /confirm/external/ usage for auth but add orcid external identity rewrite updates for v2 as well if external_id_provider == settings.EXTERNAL_IDENTITY_PROFILE.get('OrcidProfile'): user.external_identity[external_id_provider] = external_identity[external_id_provider] else: