Skip to content

2610 orcid tokens#5110

Open
everreau wants to merge 34 commits intoopenlibhums:masterfrom
everreau:2610-orcid-tokens
Open

2610 orcid tokens#5110
everreau wants to merge 34 commits intoopenlibhums:masterfrom
everreau:2610-orcid-tokens

Conversation

@everreau
Copy link
Copy Markdown
Contributor

@everreau everreau commented Jan 26, 2026

Addresses: #2610

When orcid is enabled:

  • Save orcid auth token and expiration when orcid is authenticated
  • Allow users to add orcid via authentication
  • When orcid is removed by admin or user revoke token
  • Notify users when token is not valid in profile and edit user views
  • Don't allow users to manually edit orcids.
  • Allow co-authors to request orcids
  • Add is_orcid_valid flag to FrozenAuthors
  • Add authentication element to orcid crossref xml

@everreau
Copy link
Copy Markdown
Contributor Author

supersedes: #4399

@everreau everreau requested a review from alainna January 27, 2026 19:11
@alainna
Copy link
Copy Markdown
Contributor

alainna commented Jan 28, 2026

So great, @everreau! I've suggested some changes in line with ORCID's brand guidelines

This is only for journals at the moment, is that correct? Could the co-author use the ORCID login option to connect their iD to their account rather than connecting the iD in their profile? What happens if the co-author is a frozen author without an account, or if the account isn't active?

@everreau
Copy link
Copy Markdown
Contributor Author

everreau commented Feb 2, 2026

@alainna. All of the login features also work for preprints. I don't see any reference at all to orcids in the preprint author interface. I think we (cdl) would send whatever orcid was in the author's account to crossref even though it's not visible and it appears in the preprint front end. The profile and the account are the same as far as I know. The interface on main is a bit different than what we're currently running. Maybe we should setup a time so you can take a look at what I have. I hadn't really thought about what would happen if a user connects their orcid after the frozen authors were already made. I'll take a look at that.

@ajrbyers
Copy link
Copy Markdown
Member

ajrbyers commented Feb 4, 2026

@alainna. All of the login features also work for preprints. I don't see any reference at all to orcids in the preprint author interface. I think we (cdl) would send whatever orcid was in the author's account to crossref even though it's not visible and it appears in the preprint front end. The profile and the account are the same as far as I know. The interface on main is a bit different than what we're currently running. Maybe we should setup a time so you can take a look at what I have. I hadn't really thought about what would happen if a user connects their orcid after the frozen authors were already made. I'll take a look at that.

On the FrozenAuthor object orcid is a property that will either return FrozenAuthor.frozen_orcid atrribute and if there is none check if the linked account has an orcid attribute and return that instead.

@ajrbyers
Copy link
Copy Markdown
Member

ajrbyers commented Feb 4, 2026

@joemull can you do the first technical review on this and then pass it to me please?

@joemull
Copy link
Copy Markdown
Member

joemull commented Feb 5, 2026

Just an update: I should be able to get to this later next week 16 or 17 Feb.

@alainna
Copy link
Copy Markdown
Contributor

alainna commented Feb 10, 2026

Some suggestions for this dev: eScholarship#46

Copy link
Copy Markdown
Member

@joemull joemull left a comment

Choose a reason for hiding this comment

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

This is hugely appreciated--thank you for putting it in.

I'm having trouble testing it out (an issue with OLH's ORCID sandbox that will take a few days to resolve), but I've gone ahead and given you some comments from reading the code. Hope they're clear but please do ping me if anything does not make sense.

response = self.client.get(reverse("core_edit_profile"))
self.assertContains(response, "ORCID iD could not be validated.")
self.assertContains(response, "Connect your ORCID")
self.assertContains(response, "https://sandbox.orcid.org/0000-0000-0000-0000")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this test (or any of the ones below) rely on a network connection? We'll need to put mocks into any test that relies on a network connection.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

None of the tests rely on network connection. It's setting the orcid url purely for display purposes. I noticed that ruff is failing now but it was passing at some point and the tests ran successfully.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is using a network connection, because is_orcid_token_valid calls the ORCID API using requests. I turned my internet off and got the error below. So I think we'll just want to mock it where the Janeway logic that is being tested ends, and the ORCID API code begins, in this and subsequent tests that use the API.

Traceback (most recent call last):                                                                                                                                                                                  
  File "/home/joemull/git/janepr/src/core/tests/test_app.py", line 563, in setUp                                                                                                                                    
    self.article_one = helpers.create_article(                                                                                                                                                                      
  File "/home/joemull/git/janepr/src/utils/testing/helpers.py", line 300, in create_article                                                                                                                         
    author.snapshot_as_author(article)                                                                                                                                                                              
  File "/home/joemull/git/janepr/src/core/models.py", line 893, in snapshot_as_author
    frozen_dict["is_frozen_orcid_valid"] = self.is_orcid_token_valid()
  File "/home/joemull/git/janepr/src/core/models.py", line 963, in is_orcid_token_valid
    return is_token_valid(self.orcid, self.orcid_token)
  File "/home/joemull/git/janepr/src/utils/orcid.py", line 76, in is_token_valid
    r = api_client._get_public_info(
  File "/home/joemull/git/janepr/.venv/lib/python3.10/site-packages/orcid/orcid.py", line 410, in _get_public_info
    return requests.get(request_url, headers=headers, 
  File "/home/joemull/git/janepr/.venv/lib/python3.10/site-packages/requests/api.py", line 73, in get
    return request("get", url, params=params, **kwargs)
  File "/home/joemull/git/janepr/.venv/lib/python3.10/site-packages/requests/api.py", line 59, in request
    return session.request(method=method, url=url, **kwargs)
  File "/home/joemull/git/janepr/.venv/lib/python3.10/site-packages/requests/sessions.py", line 589, in request
    resp = self.send(prep, **send_kwargs)
  File "/home/joemull/git/janepr/.venv/lib/python3.10/site-packages/requests/sessions.py", line 703, in send
    r = adapter.send(request, **kwargs)
  File "/home/joemull/git/janepr/.venv/lib/python3.10/site-packages/requests/adapters.py", line 700, in send
    raise ConnectionError(e, request=request)
requests.exceptions.ConnectionError: HTTPSConnectionPool(host='pub.orcid.org', port=443): Max retries exceeded with url: /v2.0/0004-5678-9012-345X/record (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7c9bc79e8190>: Failed to establish a new connection: [Errno -3] Temporary failure in name resolution'))

@joemull joemull assigned everreau and unassigned joemull Feb 16, 2026
@joemull joemull assigned joemull and unassigned joemull Feb 23, 2026
@alainna
Copy link
Copy Markdown
Contributor

alainna commented Mar 4, 2026

Suggested email text for the situation in which an author account exists, but may or may not be active.

The below is written with the assumption that:

  1. an author must activate their account to add an authenticated ORCID iD
  2. an inactive account must manually request a password reset/activation key to activate their account, and
  3. it's possible to add specific text for inactive accounts using if.... else

Dear {co-author name},

{object owner name} has added you as a co-author to "{object title}" in {journal / repository name} and requests that you add your ORCID iD to the publication.

[if account is not active]

First, please activate your author {journal / repository name} profile by {setting a password in the system} using the email address {email address on the account}.

Second, {click here to connect your verified ORCID iD to your account}. You may be asked to log into {journal / repository name} again.

[if account is active]

{Please click here to verify your ORCID iD and connect it to your {journal / repository name} account (registered email: {co-author email address}].}

Kind regards,
{journal / repository name }

@everreau everreau mentioned this pull request Mar 4, 2026
@everreau
Copy link
Copy Markdown
Contributor Author

@joemull I think this may be ready for review. I think the login logic in core.views is getting quite confusing. I was trying to rewrite so we can favor accounts that have authenticated orcids and prevent duplicate authenticated orcids. Maybe it would be easier to just make the orcid field unique at this point? I'm not sure if there is anything else in the queue that is needed before that.

@joemull
Copy link
Copy Markdown
Member

joemull commented Mar 17, 2026

OK @everreau I will try to get to it first thing next week (I'm out the rest of this week). Yes every time I try to read through that logic I get confused. I agree it is high time to make the ORCID unique. I think the hard thing there is that there are duplicates currently. So the data migration would have to enforce the unique constraint while also recording the duplicates somehow and making them visible to someone (press manager, editor)? to resolve.

@joemull joemull self-requested a review March 17, 2026 08:47
@joemull joemull self-assigned this Mar 17, 2026
@everreau
Copy link
Copy Markdown
Contributor Author

@joemull so, my thought on dealing with the duplicates would be to create a migration that looks for the best account to retain the orcid by a set of criteria (eg is active, matches info held in orcid, last login), remove it from the others and create a log entry that says it was removed. Then we could add some logic that would allow a user to reclaim the orcid to their own account by authenticating with orcid and we could add a view that lets managers see everything that has the given log entry and resolve manually?

Copy link
Copy Markdown
Member

@joemull joemull left a comment

Choose a reason for hiding this comment

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

This is getting really close to being ready, and it's great that you were able to loop in the frozen author piece. I took a careful read through the logic and tested it out, and it's mostly working great for me. I just have a few requests around tests, the admin view, and maintainability, and one suggestion for usability--in the verification request, taking the user straight to the verification step rather than to their profile.

response = self.client.get(reverse("core_edit_profile"))
self.assertContains(response, "ORCID iD could not be validated.")
self.assertContains(response, "Connect your ORCID")
self.assertContains(response, "https://sandbox.orcid.org/0000-0000-0000-0000")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is using a network connection, because is_orcid_token_valid calls the ORCID API using requests. I turned my internet off and got the error below. So I think we'll just want to mock it where the Janeway logic that is being tested ends, and the ORCID API code begins, in this and subsequent tests that use the API.

Traceback (most recent call last):                                                                                                                                                                                  
  File "/home/joemull/git/janepr/src/core/tests/test_app.py", line 563, in setUp                                                                                                                                    
    self.article_one = helpers.create_article(                                                                                                                                                                      
  File "/home/joemull/git/janepr/src/utils/testing/helpers.py", line 300, in create_article                                                                                                                         
    author.snapshot_as_author(article)                                                                                                                                                                              
  File "/home/joemull/git/janepr/src/core/models.py", line 893, in snapshot_as_author
    frozen_dict["is_frozen_orcid_valid"] = self.is_orcid_token_valid()
  File "/home/joemull/git/janepr/src/core/models.py", line 963, in is_orcid_token_valid
    return is_token_valid(self.orcid, self.orcid_token)
  File "/home/joemull/git/janepr/src/utils/orcid.py", line 76, in is_token_valid
    r = api_client._get_public_info(
  File "/home/joemull/git/janepr/.venv/lib/python3.10/site-packages/orcid/orcid.py", line 410, in _get_public_info
    return requests.get(request_url, headers=headers, 
  File "/home/joemull/git/janepr/.venv/lib/python3.10/site-packages/requests/api.py", line 73, in get
    return request("get", url, params=params, **kwargs)
  File "/home/joemull/git/janepr/.venv/lib/python3.10/site-packages/requests/api.py", line 59, in request
    return session.request(method=method, url=url, **kwargs)
  File "/home/joemull/git/janepr/.venv/lib/python3.10/site-packages/requests/sessions.py", line 589, in request
    resp = self.send(prep, **send_kwargs)
  File "/home/joemull/git/janepr/.venv/lib/python3.10/site-packages/requests/sessions.py", line 703, in send
    r = adapter.send(request, **kwargs)
  File "/home/joemull/git/janepr/.venv/lib/python3.10/site-packages/requests/adapters.py", line 700, in send
    raise ConnectionError(e, request=request)
requests.exceptions.ConnectionError: HTTPSConnectionPool(host='pub.orcid.org', port=443): Max retries exceeded with url: /v2.0/0004-5678-9012-345X/record (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7c9bc79e8190>: Failed to establish a new connection: [Errno -3] Temporary failure in name resolution'))

)
orcid_token = models.CharField(max_length=40, blank=True, default="")
orcid_token_expiration = models.DateTimeField(null=True, blank=True)
date_orcid_requested = models.DateTimeField(blank=True, null=True)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The admin view for the account is a bit unique in that we have to manually add the fields we want to see. I think it would be helpful to see these new ones in the admin when troubleshooting login issues for users, so could we add them in the list in AccountAdmin.fieldsets?

messages.WARNING,
_("You must be logged in to connect an ORCID iD to your account."),
)
return redirect(logic.reverse_with_next("core_login", next_url))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

OK, with my proposed change to the email template, this might catch a fair number of users. So if you like that change, let's make sure users come back here after they've done the non-ORCID login. Something like this:

next_url = request.get_full_path()
redirect(logic.reverse_with_next("core_login", next_url))

self.assertIn(
f"/register/step/orcid/",
response.redirect_chain[0][0],
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If I'm reading this right, the redirect to registration would happen here even if the account was active, because neither the ORCID nor email matches. Should we add some of those in here so it's clear the test is just testing active / inactive status?

self.assertIn(
f"/register/step/orcid/",
response.redirect_chain[0][0],
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure what this test is testing. Is it about duplicate ORCIDs, or the account being inactive? I think it basically does what I was asking for, for the previous test, but it also has a bit in the name about duplicates, so I'm not sure.

subject = "subject_orcid_request"
else:
template = "orcid_activate_request"
subject = "subject_orcid_activate_request"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we just want one of these now, since there is just one template, right?

"user": user,
"user_profile_url": request.site_type.site_url(
reverse("core_edit_profile"),
),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Have you considered making the link straight to the verification step? I think this might be clearer for users who do not remember what the profile page looks like. If they are taken to the profile page, they have to scroll down to the social media and accounts section and notice the button to connect their ORCID, but if they are taken straight to the redirect cycle, they'll be prompted at each step what to do.

We have a helper function in this file that might be useful:

reverse_with_query("core_edit_profile", { "action": "add_profile_orcid" })

return username.lower()

def get_orcid_url(self):
return f"{settings.ORCID_URL.replace('oauth/authorize', '')}{self.orcid}"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wonder what are the benefits and drawbacks of this versus the logic in orcid_uri for the frozen author (and now the preprint author)? I'm thinking of a few things:

  • The ORCID IDs in Janeway databases have sometimes been input as URLs (obviously something to fix when we make the field unique), so get_orcid_url would potentially result in repeating the host name and domain parts.
  • It might be useful in some contexts to surface the sandbox.orcid.org domain, but this is not accounted for by orcid_uri because it hard-codes the domain.

Maybe it's time to combine the logic and have one method for Account, FrozenAuthor, and PreprintAuthor?

@joemull
Copy link
Copy Markdown
Member

joemull commented Mar 26, 2026

@joemull so, my thought on dealing with the duplicates would be to create a migration that looks for the best account to retain the orcid by a set of criteria (eg is active, matches info held in orcid, last login), remove it from the others and create a log entry that says it was removed. Then we could add some logic that would allow a user to reclaim the orcid to their own account by authenticating with orcid and we could add a view that lets managers see everything that has the given log entry and resolve manually?

That does sound like a good approach to me. I think we'd open another can of worms by putting it in this PR, but I added your comment on the thread for #4751, where a few people commented on how to go about de-duping.

@joemull joemull removed their assignment Mar 26, 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.

4 participants