From 1c3344b2f781ea532ffe5ea65b7b6aa064af2e7c Mon Sep 17 00:00:00 2001 From: Vlad0n20 Date: Fri, 31 Jul 2026 16:21:28 +0200 Subject: [PATCH 1/2] Add revoke endpoint request for ORCID --- admin/templates/users/user.html | 12 +++++++ framework/auth/cas.py | 16 +++++++++ framework/auth/tasks.py | 11 ++++++ .../0045_osfuser_orcid_token_tracking.py | 27 ++++++++++++++ osf/models/user.py | 24 ++++++++++++- osf_tests/test_user.py | 35 +++++++++++++++++-- website/settings/defaults.py | 1 + 7 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 osf/migrations/0045_osfuser_orcid_token_tracking.py diff --git a/admin/templates/users/user.html b/admin/templates/users/user.html index 5cc9244d484..640bf0f9924 100644 --- a/admin/templates/users/user.html +++ b/admin/templates/users/user.html @@ -126,6 +126,18 @@

User: {{ user.username }} ({{user. {% endif %} + + Initial ORCID Authorization + {{ user.date_orcid_initial_authorized }} + + + Last ORCID Authorization + {{ user.date_orcid_last_authorized }} + + + ORCID Token Reserved + {{ user.orcid_token_stored }} + Registered {{ user.is_registered }} [{{ user.date_registered }}] diff --git a/framework/auth/cas.py b/framework/auth/cas.py index 1084739fdc3..a4d576868a9 100644 --- a/framework/auth/cas.py +++ b/framework/auth/cas.py @@ -28,6 +28,7 @@ class CasHTTPError(CasError): def __init__(self, code, message, headers, content): super().__init__(code, message) + self.message = message self.headers = headers self.content = content @@ -97,6 +98,10 @@ def get_auth_token_revocation_url(self): url = furl(self.BASE_URL).add(path=['oauth2', 'revoke']) return url.url + def get_orcid_token_revocation_url(self): + url = furl(self.BASE_URL).add(path=['osf', 'orcid', 'revoke']) + return url.url + def service_validate(self, ticket, service_url): """ Send request to CAS to validate ticket. @@ -198,6 +203,17 @@ def revoke_tokens(self, payload): else: self._handle_error(resp) + def revoke_orcid_token(self, orcid_id): + url = self.get_orcid_token_revocation_url() + headers = { + 'Authorization': f'Bearer {settings.CAS_ORCID_REVOKE_SHARED_SECRET}', + } + resp = requests.post(url, json={'orcid_id': orcid_id}, headers=headers) + if resp.status_code == 204: + return True + else: + self._handle_error(resp) + def parse_auth_header(header): """ diff --git a/framework/auth/tasks.py b/framework/auth/tasks.py index e0083798911..640003fd994 100644 --- a/framework/auth/tasks.py +++ b/framework/auth/tasks.py @@ -2,6 +2,7 @@ import itertools import logging +from django.utils import timezone from lxml import etree import pytz import requests @@ -54,6 +55,16 @@ def update_affiliation_for_orcid_sso_users(user_id, orcid_id): logger.error(error_message) sentry.log_message(error_message) return + + # Best-effort tracking of ORCID (re)authorization for admin visibility and GDPR-delete triage. + # This reflects that OSF observed a completed ORCID login, not a confirmed CAS-side token write. + now = timezone.now() + if not user.date_orcid_initial_authorized: + user.date_orcid_initial_authorized = now + user.date_orcid_last_authorized = now + user.orcid_token_stored = True + user.save() + institution = check_institution_affiliation(orcid_id) if institution: logger.info(f'Eligible institution affiliation has been found for ORCiD SSO user: ' diff --git a/osf/migrations/0045_osfuser_orcid_token_tracking.py b/osf/migrations/0045_osfuser_orcid_token_tracking.py new file mode 100644 index 00000000000..c4519083a1a --- /dev/null +++ b/osf/migrations/0045_osfuser_orcid_token_tracking.py @@ -0,0 +1,27 @@ +from django.db import migrations, models +import osf.utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('osf', '0044_notification_scheduled'), + ] + + operations = [ + migrations.AddField( + model_name='osfuser', + name='date_orcid_initial_authorized', + field=osf.utils.fields.NonNaiveDateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='osfuser', + name='date_orcid_last_authorized', + field=osf.utils.fields.NonNaiveDateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='osfuser', + name='orcid_token_stored', + field=models.BooleanField(default=False), + ), + ] diff --git a/osf/models/user.py b/osf/models/user.py index 0218af34692..f8be973d9b5 100644 --- a/osf/models/user.py +++ b/osf/models/user.py @@ -26,7 +26,7 @@ from django.utils import timezone from framework import sentry -from framework.auth import Auth, signals, utils +from framework.auth import Auth, cas, signals, utils from framework.auth.core import generate_verification_key from framework.auth.exceptions import ( ChangePasswordError, @@ -402,6 +402,12 @@ class OSFUser(DirtyFieldsMixin, GuidMixin, BaseModel, AbstractBaseUser, Permissi chronos_user_id = models.TextField(null=True, blank=True, db_index=True) + date_orcid_initial_authorized = NonNaiveDateTimeField(null=True, blank=True) + + date_orcid_last_authorized = NonNaiveDateTimeField(null=True, blank=True) + + orcid_token_stored = models.BooleanField(default=False) + allow_indexing = models.BooleanField(null=True, blank=True, default=None) objects = OSFUserManager() @@ -2166,6 +2172,22 @@ def _clear_identifying_information(self): account.profile_url = None account.save() self.external_accounts.clear() + + # Revoke any ORCID OAuth token CAS holds for this user, so OSF no longer shows as a + # trusted party on the user's ORCID account. Best-effort: never blocks GDPR delete. + orcid_ids = self.external_identity.get('ORCID', {}) + if orcid_ids: + for orcid_id in orcid_ids: + try: + cas.get_client().revoke_orcid_token(orcid_id) + except cas.CasHTTPError as e: + logger.error(f'Unable to revoke ORCID token via CAS for user {self._id}, orcid_id={orcid_id}: {e}') + sentry.log_exception(e) + except Exception as e: + logger.error(f'Unexpected error revoking ORCID token via CAS for user {self._id}, orcid_id={orcid_id}: {e}') + sentry.log_exception(e) + self.orcid_token_stored = False + self.external_identity = {} self.deleted = timezone.now() diff --git a/osf_tests/test_user.py b/osf_tests/test_user.py index 27a447d6681..fed7cc03f17 100644 --- a/osf_tests/test_user.py +++ b/osf_tests/test_user.py @@ -2222,11 +2222,15 @@ def test_gdpr_delete_triggers_share_update_for_public_shared_preprints( assert mock_update_search.called - def test_can_gdpr_delete(self, user): + @mock.patch('framework.auth.cas.CasClient.revoke_orcid_token') + def test_can_gdpr_delete(self, mock_revoke_orcid_token, user): + user.external_identity = {'ORCID': {'fake-orcid-id': 'VERIFIED'}} + user.orcid_token_stored = True + user.save() + user.social = ['fake social'] user.schools = ['fake schools'] user.jobs = ['fake jobs'] - user.external_identity = ['fake external identity'] user.external_accounts.add(ExternalAccountFactory()) user.gdpr_delete() @@ -2241,6 +2245,33 @@ def test_can_gdpr_delete(self, user): assert not user.external_accounts.exists() assert user.is_disabled assert user.deleted is not None + mock_revoke_orcid_token.assert_called_once_with('fake-orcid-id') + assert user.orcid_token_stored is False + + @mock.patch('framework.auth.cas.CasClient.revoke_orcid_token') + def test_gdpr_delete_no_orcid_no_cas_call(self, mock_revoke_orcid_token, user): + assert user.external_identity == {} + + user.gdpr_delete() + + mock_revoke_orcid_token.assert_not_called() + + @mock.patch('framework.auth.cas.CasClient.revoke_orcid_token') + def test_gdpr_delete_orcid_revoke_failure_does_not_block_delete(self, mock_revoke_orcid_token, user): + from framework.auth import cas + mock_revoke_orcid_token.side_effect = cas.CasHTTPError( + code=400, message='Bad Request', headers={}, content=b'', + ) + user.external_identity = {'ORCID': {'fake-orcid-id': 'VERIFIED'}} + user.orcid_token_stored = True + user.save() + + user.gdpr_delete() + + mock_revoke_orcid_token.assert_called_once_with('fake-orcid-id') + assert user.external_identity == {} + assert user.orcid_token_stored is False + assert user.deleted is not None def test_can_gdpr_delete_personal_nodes(self, user): diff --git a/website/settings/defaults.py b/website/settings/defaults.py index 026187948a5..b0ad777a7b0 100644 --- a/website/settings/defaults.py +++ b/website/settings/defaults.py @@ -383,6 +383,7 @@ def parent_dir(path): SPAM_SUBMIT_TASK_HARD_TIME_LIMIT = 90 CAS_SERVER_URL = 'http://localhost:8080' +CAS_ORCID_REVOKE_SHARED_SECRET = os.environ.get('CAS_ORCID_REVOKE_SHARED_SECRET', 'changeme') MFR_SERVER_URL = 'http://localhost:7778' ###### ARCHIVER ########### From 4e75841ae6d30106f2cbc89abf4723329721f3d8 Mon Sep 17 00:00:00 2001 From: Vlad0n20 Date: Fri, 7 Aug 2026 17:29:47 +0200 Subject: [PATCH 2/2] Update ORCID revoke workflow --- admin/templates/users/user.html | 12 ---- framework/auth/cas.py | 29 ++++---- framework/auth/tasks.py | 10 --- .../0045_osfuser_orcid_token_tracking.py | 27 -------- osf/models/user.py | 39 +++++------ osf_tests/test_user.py | 68 ++++++++++++------- tests/test_cas_authentication.py | 62 ++++++++++++++++- website/settings/defaults.py | 5 +- 8 files changed, 140 insertions(+), 112 deletions(-) delete mode 100644 osf/migrations/0045_osfuser_orcid_token_tracking.py diff --git a/admin/templates/users/user.html b/admin/templates/users/user.html index 640bf0f9924..5cc9244d484 100644 --- a/admin/templates/users/user.html +++ b/admin/templates/users/user.html @@ -126,18 +126,6 @@

User: {{ user.username }} ({{user. {% endif %} - - Initial ORCID Authorization - {{ user.date_orcid_initial_authorized }} - - - Last ORCID Authorization - {{ user.date_orcid_last_authorized }} - - - ORCID Token Reserved - {{ user.orcid_token_stored }} - Registered {{ user.is_registered }} [{{ user.date_registered }}] diff --git a/framework/auth/cas.py b/framework/auth/cas.py index a4d576868a9..26da523e8e2 100644 --- a/framework/auth/cas.py +++ b/framework/auth/cas.py @@ -28,7 +28,6 @@ class CasHTTPError(CasError): def __init__(self, code, message, headers, content): super().__init__(code, message) - self.message = message self.headers = headers self.content = content @@ -98,10 +97,6 @@ def get_auth_token_revocation_url(self): url = furl(self.BASE_URL).add(path=['oauth2', 'revoke']) return url.url - def get_orcid_token_revocation_url(self): - url = furl(self.BASE_URL).add(path=['osf', 'orcid', 'revoke']) - return url.url - def service_validate(self, ticket, service_url): """ Send request to CAS to validate ticket. @@ -203,17 +198,6 @@ def revoke_tokens(self, payload): else: self._handle_error(resp) - def revoke_orcid_token(self, orcid_id): - url = self.get_orcid_token_revocation_url() - headers = { - 'Authorization': f'Bearer {settings.CAS_ORCID_REVOKE_SHARED_SECRET}', - } - resp = requests.post(url, json={'orcid_id': orcid_id}, headers=headers) - if resp.status_code == 204: - return True - else: - self._handle_error(resp) - def parse_auth_header(header): """ @@ -305,6 +289,19 @@ def make_response_from_ticket(ticket, service_url): if tos_checked_via_cas: user_updates['accepted_terms_of_service'] = timezone.now() print_cas_log(f'CAS TOS consent checked: {user.guids.first()._id}, {user.username}', LogLevel.INFO) + orcid_id = cas_resp.attributes.get('orcidId') + orcid_access_token = cas_resp.attributes.get('orcidAccessToken') + if orcid_id and orcid_access_token: + from osf.models.external import ExternalAccount + account, _ = ExternalAccount.objects.update_or_create( + provider='orcid', + provider_id=orcid_id, + defaults={ + 'provider_name': 'ORCID', + 'oauth_key': orcid_access_token, + }, + ) + user.external_accounts.add(account) # if we successfully authenticate and a verification key is present, invalidate it if user.verification_key: user_updates['verification_key'] = None diff --git a/framework/auth/tasks.py b/framework/auth/tasks.py index 640003fd994..fb8a0959ff5 100644 --- a/framework/auth/tasks.py +++ b/framework/auth/tasks.py @@ -2,7 +2,6 @@ import itertools import logging -from django.utils import timezone from lxml import etree import pytz import requests @@ -56,15 +55,6 @@ def update_affiliation_for_orcid_sso_users(user_id, orcid_id): sentry.log_message(error_message) return - # Best-effort tracking of ORCID (re)authorization for admin visibility and GDPR-delete triage. - # This reflects that OSF observed a completed ORCID login, not a confirmed CAS-side token write. - now = timezone.now() - if not user.date_orcid_initial_authorized: - user.date_orcid_initial_authorized = now - user.date_orcid_last_authorized = now - user.orcid_token_stored = True - user.save() - institution = check_institution_affiliation(orcid_id) if institution: logger.info(f'Eligible institution affiliation has been found for ORCiD SSO user: ' diff --git a/osf/migrations/0045_osfuser_orcid_token_tracking.py b/osf/migrations/0045_osfuser_orcid_token_tracking.py deleted file mode 100644 index c4519083a1a..00000000000 --- a/osf/migrations/0045_osfuser_orcid_token_tracking.py +++ /dev/null @@ -1,27 +0,0 @@ -from django.db import migrations, models -import osf.utils.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('osf', '0044_notification_scheduled'), - ] - - operations = [ - migrations.AddField( - model_name='osfuser', - name='date_orcid_initial_authorized', - field=osf.utils.fields.NonNaiveDateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name='osfuser', - name='date_orcid_last_authorized', - field=osf.utils.fields.NonNaiveDateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name='osfuser', - name='orcid_token_stored', - field=models.BooleanField(default=False), - ), - ] diff --git a/osf/models/user.py b/osf/models/user.py index f8be973d9b5..1fc9649eca8 100644 --- a/osf/models/user.py +++ b/osf/models/user.py @@ -12,6 +12,7 @@ # OSF imports import itsdangerous import pytz +import requests from dirtyfields import DirtyFieldsMixin from django.conf import settings @@ -26,7 +27,7 @@ from django.utils import timezone from framework import sentry -from framework.auth import Auth, cas, signals, utils +from framework.auth import Auth, signals, utils from framework.auth.core import generate_verification_key from framework.auth.exceptions import ( ChangePasswordError, @@ -402,12 +403,6 @@ class OSFUser(DirtyFieldsMixin, GuidMixin, BaseModel, AbstractBaseUser, Permissi chronos_user_id = models.TextField(null=True, blank=True, db_index=True) - date_orcid_initial_authorized = NonNaiveDateTimeField(null=True, blank=True) - - date_orcid_last_authorized = NonNaiveDateTimeField(null=True, blank=True) - - orcid_token_stored = models.BooleanField(default=False) - allow_indexing = models.BooleanField(null=True, blank=True, default=None) objects = OSFUserManager() @@ -2160,6 +2155,21 @@ def _clear_identifying_information(self): self.social = {} self.unclaimed_records = {} self.notifications_configured = {} + # Revoke ORCID OAuth grant before scrubbing the token below (best-effort, does not block deletion) + orcid_accounts = self.external_accounts.filter(provider='orcid') + for account in orcid_accounts: + try: + requests.post( + website_settings.ORCID_OAUTH_REVOKE_URL, + data={ + 'client_id': website_settings.ORCID_OAUTH_CLIENT_ID, + 'client_secret': website_settings.ORCID_OAUTH_CLIENT_SECRET, + 'token': account.oauth_key, + }, + timeout=5, + ) + except requests.exceptions.RequestException as e: + logger.warning(f'Failed to revoke ORCID token for user {self._id}: {e}') # Scrub all external accounts if self.external_accounts.exists(): logger.info('Clearing identifying information from external accounts...') @@ -2173,21 +2183,6 @@ def _clear_identifying_information(self): account.save() self.external_accounts.clear() - # Revoke any ORCID OAuth token CAS holds for this user, so OSF no longer shows as a - # trusted party on the user's ORCID account. Best-effort: never blocks GDPR delete. - orcid_ids = self.external_identity.get('ORCID', {}) - if orcid_ids: - for orcid_id in orcid_ids: - try: - cas.get_client().revoke_orcid_token(orcid_id) - except cas.CasHTTPError as e: - logger.error(f'Unable to revoke ORCID token via CAS for user {self._id}, orcid_id={orcid_id}: {e}') - sentry.log_exception(e) - except Exception as e: - logger.error(f'Unexpected error revoking ORCID token via CAS for user {self._id}, orcid_id={orcid_id}: {e}') - sentry.log_exception(e) - self.orcid_token_stored = False - self.external_identity = {} self.deleted = timezone.now() diff --git a/osf_tests/test_user.py b/osf_tests/test_user.py index fed7cc03f17..543ad934751 100644 --- a/osf_tests/test_user.py +++ b/osf_tests/test_user.py @@ -12,6 +12,8 @@ from unittest import mock import itsdangerous import pytest +import requests +import responses from importlib import import_module from framework.auth.exceptions import ExpiredTokenError, InvalidTokenError, ChangePasswordError @@ -2222,15 +2224,11 @@ def test_gdpr_delete_triggers_share_update_for_public_shared_preprints( assert mock_update_search.called - @mock.patch('framework.auth.cas.CasClient.revoke_orcid_token') - def test_can_gdpr_delete(self, mock_revoke_orcid_token, user): - user.external_identity = {'ORCID': {'fake-orcid-id': 'VERIFIED'}} - user.orcid_token_stored = True - user.save() - + def test_can_gdpr_delete(self, user): user.social = ['fake social'] user.schools = ['fake schools'] user.jobs = ['fake jobs'] + user.external_identity = ['fake external identity'] user.external_accounts.add(ExternalAccountFactory()) user.gdpr_delete() @@ -2245,33 +2243,57 @@ def test_can_gdpr_delete(self, mock_revoke_orcid_token, user): assert not user.external_accounts.exists() assert user.is_disabled assert user.deleted is not None - mock_revoke_orcid_token.assert_called_once_with('fake-orcid-id') - assert user.orcid_token_stored is False - @mock.patch('framework.auth.cas.CasClient.revoke_orcid_token') - def test_gdpr_delete_no_orcid_no_cas_call(self, mock_revoke_orcid_token, user): - assert user.external_identity == {} + @responses.activate + def test_gdpr_delete_revokes_orcid_token(self, user): + responses.add( + responses.POST, + settings.ORCID_OAUTH_REVOKE_URL, + status=200, + ) + account = ExternalAccountFactory(provider='orcid', oauth_key='fake-orcid-token') + user.external_accounts.add(account) user.gdpr_delete() - mock_revoke_orcid_token.assert_not_called() + assert len(responses.calls) == 1 + request_body = responses.calls[0].request.body + assert f'token={account.oauth_key}' in request_body + assert f'client_id={settings.ORCID_OAUTH_CLIENT_ID}' in request_body + assert f'client_secret={settings.ORCID_OAUTH_CLIENT_SECRET}' in request_body + account.reload() + assert account.oauth_key is None + + @responses.activate + def test_gdpr_delete_no_orcid_account_no_revoke_call(self, user): + responses.add( + responses.POST, + settings.ORCID_OAUTH_REVOKE_URL, + status=200, + ) + user.external_accounts.add(ExternalAccountFactory(provider='github')) - @mock.patch('framework.auth.cas.CasClient.revoke_orcid_token') - def test_gdpr_delete_orcid_revoke_failure_does_not_block_delete(self, mock_revoke_orcid_token, user): - from framework.auth import cas - mock_revoke_orcid_token.side_effect = cas.CasHTTPError( - code=400, message='Bad Request', headers={}, content=b'', + user.gdpr_delete() + + assert len(responses.calls) == 0 + + @responses.activate + def test_gdpr_delete_orcid_revoke_failure_does_not_block_delete(self, user): + responses.add( + responses.POST, + settings.ORCID_OAUTH_REVOKE_URL, + body=requests.exceptions.ConnectionError('boom'), ) - user.external_identity = {'ORCID': {'fake-orcid-id': 'VERIFIED'}} - user.orcid_token_stored = True - user.save() + account = ExternalAccountFactory(provider='orcid', oauth_key='fake-orcid-token') + user.external_accounts.add(account) user.gdpr_delete() - mock_revoke_orcid_token.assert_called_once_with('fake-orcid-id') - assert user.external_identity == {} - assert user.orcid_token_stored is False + assert len(responses.calls) == 1 assert user.deleted is not None + assert not user.external_accounts.exists() + account.reload() + assert account.oauth_key is None def test_can_gdpr_delete_personal_nodes(self, user): diff --git a/tests/test_cas_authentication.py b/tests/test_cas_authentication.py index a272bfe4feb..2a5786da625 100644 --- a/tests/test_cas_authentication.py +++ b/tests/test_cas_authentication.py @@ -6,9 +6,10 @@ from framework.auth import cas +from osf.models import ExternalAccount from tests.base import OsfTestCase, fake from tests.utils import run_celery_tasks -from osf_tests.factories import UserFactory +from osf_tests.factories import ExternalAccountFactory, UserFactory def make_successful_response(user): @@ -32,6 +33,18 @@ def make_successful_response_with_tos_consent(user): ) +def make_successful_response_with_orcid_attrs(user, orcid_id=None, access_token=None): + return cas.CasResponse( + authenticated=True, + user=user._id, + attributes={ + 'accessToken': fake.md5(), + 'orcidId': orcid_id or fake.numerify('####-####-####-####'), + 'orcidAccessToken': access_token or fake.md5(), + } + ) + + def make_failure_response(): return cas.CasResponse( authenticated=False, @@ -261,6 +274,53 @@ def test_make_response_from_ticket_success_with_tos_consent(self, mock_service_v assert mock_service_validate.call_count == 1 assert mock_get_user_from_cas_resp.call_count == 1 + @mock.patch('framework.auth.cas.get_user_from_cas_resp') + @mock.patch('framework.auth.cas.CasClient.service_validate') + def test_make_response_from_ticket_success_with_orcid_attrs(self, mock_service_validate, mock_get_user_from_cas_resp): + orcid_id = fake.numerify('####-####-####-####') + access_token = fake.md5() + mock_service_validate.return_value = make_successful_response_with_orcid_attrs( + self.user, orcid_id=orcid_id, access_token=access_token + ) + mock_get_user_from_cas_resp.return_value = (self.user, None, 'authenticate') + ticket = fake.md5() + service_url = 'http://localhost:5000/' + resp = cas.make_response_from_ticket(ticket, service_url) + assert resp.status_code == 302 + account = ExternalAccount.objects.get(provider='orcid', provider_id=orcid_id) + assert account.oauth_key == access_token + assert account in self.user.external_accounts.all() + + @mock.patch('framework.auth.cas.get_user_from_cas_resp') + @mock.patch('framework.auth.cas.CasClient.service_validate') + def test_make_response_from_ticket_updates_existing_orcid_account(self, mock_service_validate, mock_get_user_from_cas_resp): + orcid_id = fake.numerify('####-####-####-####') + existing_account = ExternalAccountFactory(provider='orcid', provider_id=orcid_id, oauth_key='old-token') + self.user.external_accounts.add(existing_account) + new_access_token = fake.md5() + mock_service_validate.return_value = make_successful_response_with_orcid_attrs( + self.user, orcid_id=orcid_id, access_token=new_access_token + ) + mock_get_user_from_cas_resp.return_value = (self.user, None, 'authenticate') + ticket = fake.md5() + service_url = 'http://localhost:5000/' + resp = cas.make_response_from_ticket(ticket, service_url) + assert resp.status_code == 302 + assert ExternalAccount.objects.filter(provider='orcid', provider_id=orcid_id).count() == 1 + existing_account.reload() + assert existing_account.oauth_key == new_access_token + + @mock.patch('framework.auth.cas.get_user_from_cas_resp') + @mock.patch('framework.auth.cas.CasClient.service_validate') + def test_make_response_from_ticket_no_orcid_attrs_no_account_created(self, mock_service_validate, mock_get_user_from_cas_resp): + mock_service_validate.return_value = make_successful_response(self.user) + mock_get_user_from_cas_resp.return_value = (self.user, None, 'authenticate') + ticket = fake.md5() + service_url = 'http://localhost:5000/' + resp = cas.make_response_from_ticket(ticket, service_url) + assert resp.status_code == 302 + assert not ExternalAccount.objects.filter(provider='orcid').exists() + @mock.patch('framework.auth.cas.get_user_from_cas_resp') @mock.patch('framework.auth.cas.CasClient.service_validate') def test_make_response_from_ticket_failure(self, mock_service_validate, mock_get_user_from_cas_resp): diff --git a/website/settings/defaults.py b/website/settings/defaults.py index b0ad777a7b0..88ba9464df7 100644 --- a/website/settings/defaults.py +++ b/website/settings/defaults.py @@ -383,7 +383,6 @@ def parent_dir(path): SPAM_SUBMIT_TASK_HARD_TIME_LIMIT = 90 CAS_SERVER_URL = 'http://localhost:8080' -CAS_ORCID_REVOKE_SHARED_SECRET = os.environ.get('CAS_ORCID_REVOKE_SHARED_SECRET', 'changeme') MFR_SERVER_URL = 'http://localhost:7778' ###### ARCHIVER ########### @@ -797,6 +796,10 @@ class CeleryConfig: ORCID_RECORD_EMPLOYMENT_PATH = '/employments' ORCID_RECORD_EDUCATION_PATH = '/educations' +ORCID_OAUTH_CLIENT_ID = os.environ.get('ORCID_OAUTH_CLIENT_ID', 'changeme') +ORCID_OAUTH_CLIENT_SECRET = os.environ.get('ORCID_OAUTH_CLIENT_SECRET', 'changeme') +ORCID_OAUTH_REVOKE_URL = os.environ.get('ORCID_OAUTH_REVOKE_URL', 'https://sandbox.orcid.org/oauth/revoke') + # Source: https://github.com/maxd/fake_email_validator/blob/master/config/fake_domains.list BLACKLISTED_DOMAINS = [ '0-mail.com',