Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ORCID: Support authenticated ORCIDs in account profile #11222

Open
wants to merge 22 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/release-notes/11222-ORCID in profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Dataverse now includes improved integration with ORCID, supported through a grant to GDCC from the ([ORCID Global Participation Fund](https://info.orcid.org/global-participation-fund-announces-fourth-round-of-awardees/).)[https://info.orcid.org/global-participation-fund-announces-fourth-round-of-awardees/].

Specifically, Dataverse users can now link their Dataverse account with their ORCID profile which then allows Dataverse to use automatically add their ORCID to their author metadata when they create a dataset.

This functionality leverages Dataverse's existing support for login via ORCID, but can be turned on independently of it. If ORCID login is enabled, the user's ORCID will automatically be added to their profile. If the user has logged in via some other mechanism, they are able to click a button to initiate a similar authentication process in which the user must login to their ORCID account and approve the connection.

Feedback from installations that enable this functionlity is requested and we expect that updates can be made in the next Dataverse release.
1 change: 1 addition & 0 deletions doc/sphinx-guides/source/installation/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ Installation Guide
shibboleth
oauth2
oidc
orcid
external-tools
advanced
24 changes: 24 additions & 0 deletions doc/sphinx-guides/source/installation/orcid.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
ORCID Integration
=================

.. contents:: |toctitle|
:local:

Introduction
------------

Dataverse leverages ORCIDs (and other types of persistent identifiers (PIDs)) to improve the findability of data and to simplify the process of adding metadata.
When ORCIDs are included as metadata about authors, Dataverse includes them in metadata exports, advertises them through SignPosting and via metadata embedded in dataset pages, and includes them in the metadata associated with dataset DOIs.

Dataverse can be configured to make it easier to include ORCIDs
- via use of an ORCID "External Vocabulary Script" that allows users to lookup authors, depositors, etc. based on their ORCID profile metadata and then records these ORCIDs automatically and adds links to ORCID profiles in metadat displays. With this configured, there is no need enter ORCIDs directly.
- via association of ORCIDs with Dataverse user accounts, through the use of ORCID logins or, in addition or instead, a separate authenticated ORCID linking mechanism. When an ORCID is associated with a Dataverse account, it will automatically be added to the dataset metadata when a user creates a dataset and is added as an initial author.

Configuration
--------------

The steps needed to configure Dataverse to support lookup of ORCIDs for the author metadata field (and ROR identifiers for organizations as author affiliations) is described in the `Dataverse Author Field Example page<https://github.com/gdcc/dataverse-external-vocab-support/blob/main/examples/authorIDandAffilationUsingORCIDandROR.md>` in the `Dataverse External Vocabulary Suport Github Repository <https://github.com/gdcc/dataverse-external-vocab-support>`. Briefly this involves changing the :ref:`:CVocConf` setting and potentially creating local web-acessible copies of the relevant scripts.

To configure Dataverse to support adding ORCIDs to user profiles, one must configure ORCID as an OAuth2 provider as described in :doc:`oauth2`. The ability to link ORCIDs to user accounts is automatically enabled if an ORCID provider is configured. To avoid also enabling ORCID login, the provider can be registered with "enabled":false.


14 changes: 14 additions & 0 deletions doc/sphinx-guides/source/user/account.rst
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,20 @@ Microsoft Azure AD, GitHub, and Google Log In

You can also convert your Dataverse installation account to use authentication provided by GitHub, Microsoft, or Google. These options may be found in the "Other options" section of the log in page, and function similarly to how ORCID is outlined above. If you would like to convert your account away from using one of these services for log in, then you can follow the same steps as listed above for converting away from the ORCID log in.


.. _orcid-integration:

Linking ORCID with your Account Profile
---------------------------------------

If you login using ORCID, Dataverse will add the link to your ORCID account in your account profile and, when you create datasets, will automatically add you, with your ORCID, as an author.

If you login via other methods, you can add a link to your ORCID account as you create an account or later via the "Account Information" page.
As when using ORCID login, you will be redirected to the ORCID website to login there and allow the connection with Dataverse.
Once you've done that, the link to your ORCID will be shown in the Account Information page and your ORCID will be added as your identifier when you create datasets (exactly the same as if you had logged in via ORCID).

Note that the ability to login via ORCID (or other providers) and the ability to link to your ORCID profile are separate configuration options available to Dataverse administrators.

.. _my-data:

My Data
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/edu/harvard/iq/dataverse/DatasetPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -2610,7 +2610,7 @@ private void resetVersionUI() {
}
}

String creatorOrcidId = au.getOrcidId();
String creatorOrcidId = au.getAuthenticatedOrcid();
if (dsf.getDatasetFieldType().getName().equals(DatasetFieldConstant.author) && dsf.isEmpty()) {
for (DatasetFieldCompoundValue authorValue : dsf.getDatasetFieldCompoundValues()) {
for (DatasetField subField : authorValue.getChildDatasetFields()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@
import edu.harvard.iq.dataverse.authorization.providers.builtin.DataverseUserPage;
import edu.harvard.iq.dataverse.authorization.providers.oauth2.AbstractOAuth2AuthenticationProvider;
import edu.harvard.iq.dataverse.authorization.providers.shib.ShibAuthenticationProvider;
import edu.harvard.iq.dataverse.util.SystemConfig;

import java.util.Collection;
import java.util.logging.Logger;

public class AuthUtil {

private static final Logger logger = Logger.getLogger(DataverseUserPage.class.getCanonicalName());

public static boolean isNonLocalLoginEnabled(Collection<AuthenticationProvider> providers) {
public static boolean isNonLocalSignupEnabled(Collection<AuthenticationProvider> providers, SystemConfig systemConfig) {
if (providers != null) {

for (AuthenticationProvider provider : providers) {
if (provider instanceof AbstractOAuth2AuthenticationProvider || provider instanceof ShibAuthenticationProvider) {
logger.fine("found an remote auth provider (returning true): " + provider.getId());
return true;
if(!systemConfig.isSignupDisabledForRemoteAuthProvider(provider.getId())) {
return true;
}
} else {
logger.fine("not a remote auth provider: " + provider.getId());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,29 @@ public class AuthenticatedUserDisplayInfo extends RoleAssigneeDisplayInfo {
@NotBlank(message = "{user.firstName}")
private String firstName;
private String position;
private String orcid;

/*
* @todo Shouldn't we persist the displayName too? It still exists on the
* authenticateduser table.
*/
public AuthenticatedUserDisplayInfo(String firstName, String lastName, String emailAddress, String affiliation, String position) {
this(firstName, lastName, emailAddress, affiliation, position, null);
}
public AuthenticatedUserDisplayInfo(String firstName, String lastName, String emailAddress, String affiliation, String position, String orcid) {
super(firstName + " " + lastName,emailAddress,affiliation);
this.firstName = firstName;
this.lastName = lastName;
this.position = position;
this.position = position;
this.orcid = orcid;
}

public AuthenticatedUserDisplayInfo() {
super("","","");
firstName="";
lastName="";
position="";
orcid=null;
}


Expand All @@ -39,7 +45,7 @@ public AuthenticatedUserDisplayInfo() {
* @param src the display info {@code this} will be a copy of.
*/
public AuthenticatedUserDisplayInfo( AuthenticatedUserDisplayInfo src ) {
this( src.getFirstName(), src.getLastName(), src.getEmailAddress(), src.getAffiliation(), src.getPosition());
this( src.getFirstName(), src.getLastName(), src.getEmailAddress(), src.getAffiliation(), src.getPosition(), src.getOrcid());
}

public String getLastName() {
Expand Down Expand Up @@ -98,6 +104,27 @@ public boolean equals(Object obj) {
}
return Objects.equals(this.position, other.position) && super.equals(obj);
}

public void setOrcid(String orcidUrl) {
this.orcid=orcidUrl;
}

public String getOrcid() {
return orcid;
}

public String getOrcidForDisplay() {
String orcidUrl = getOrcid();
if(orcidUrl == null) {
return null;
}
int index = orcidUrl.lastIndexOf('/');
if (index > 0) {
return orcidUrl.substring(index + 1);
} else {
return orcidUrl;
}
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,6 @@ public interface AuthenticationProvider {
default boolean isUserInfoUpdateAllowed() { return false; };
default boolean isUserDeletionAllowed() { return false; };
default boolean isOAuthProvider() { return false; };
/** @todo Consider moving some or all of these to AuthenticationProviderDisplayInfo.*/
/** The identifier is only displayed in the UI if it's meaningful, such as an ORCID iD.*/
default boolean isDisplayIdentifier() { return false; };
/** ORCID calls their persistent id an "ORCID iD".*/
default String getPersistentIdName() { return null; };
/** ORCID has special language to describe their ID: http://members.orcid.org/logos-web-graphics */
default String getPersistentIdDescription() { return null; };
/** An ORCID example would be the "http://orcid.org/" part of http://orcid.org/0000-0002-7874-374X*/
default String getPersistentIdUrlPrefix() { return null; };
default String getLogo() { return null; };



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean;
import edu.harvard.iq.dataverse.authorization.providers.oauth2.AbstractOAuth2AuthenticationProvider;
import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2AuthenticationProviderFactory;
import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.OrcidOAuth2AP;
import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthenticationProviderFactory;
import edu.harvard.iq.dataverse.authorization.providers.shib.ShibAuthenticationProviderFactory;
import edu.harvard.iq.dataverse.settings.JvmSettings;
Expand Down Expand Up @@ -87,6 +88,8 @@ public class AuthenticationProvidersRegistrationServiceBean {

@PersistenceContext(unitName = "VDCNet-ejbPU")
private EntityManager em;

private AuthenticationProvider orcidProvider;

// does this method also need an explicit @Lock(WRITE)?
// - I'm assuming not; since it's guaranteed to only be called once,
Expand All @@ -110,8 +113,9 @@ public void startup() {
}

// Now, load the providers.
em.createNamedQuery("AuthenticationProviderRow.findAllEnabled", AuthenticationProviderRow.class)
em.createNamedQuery("AuthenticationProviderRow.findAll", AuthenticationProviderRow.class)
.getResultList().forEach((row) -> {
if(row.isEnabled()) {
try {
registerProvider( loadProvider(row) );

Expand All @@ -120,9 +124,28 @@ public void startup() {

} catch (AuthorizationSetupException ex) {
logger.log(Level.SEVERE, "Exception setting up the authentication provider '" + row.getId() + "': " + ex.getMessage(), ex);
}
} else {
// We still use an ORCID provider that is not enabled for login as a way to
// authenticate ORCIDs being added to account profiles
Map<String, String> data = OAuth2AuthenticationProviderFactory
.parseFactoryData(row.getFactoryData());
if ("orcid".equals(data.get("type"))) {
try {
setOrcidProvider(loadProvider(row));
} catch (Exception e) {
logger.log(Level.SEVERE, "Cannot register ORCID provider '" + row.getId());
}
}
}
});

});
// If there is an enabled ORCID provider, we'll still use that in preference to a disabled one (there should only be one but this would handle a case where, for example, someone has a disabled sandbox ORCID provider and a real enabled ORCID provider)
// Could be changed in the future if there's a need for two different clients for login and adding ORCIDs to profiles
for (AuthenticationProvider provider : authenticationProviders.values()) {
if (provider instanceof OrcidOAuth2AP) {
setOrcidProvider(provider);
}
}
// Add providers registered via MPCONFIG
if (JvmSettings.OIDC_ENABLED.lookupOptional(Boolean.class).orElse(false)) {
try {
Expand All @@ -133,6 +156,15 @@ public void startup() {
}
}

private void setOrcidProvider(AuthenticationProvider provider) {
orcidProvider = provider;

}

public AuthenticationProvider getOrcidProvider() {
return orcidProvider;
}

private void registerProviderFactory(AuthenticationProviderFactory aFactory)
throws AuthorizationSetupException
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import edu.harvard.iq.dataverse.authorization.exceptions.AuthorizationException;
import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2Exception;
import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2UserRecord;
import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.OrcidOAuth2AP;
import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthProvider;
import edu.harvard.iq.dataverse.search.IndexServiceBean;
import edu.harvard.iq.dataverse.actionlogging.ActionLogRecord;
Expand Down Expand Up @@ -1043,4 +1044,27 @@ private List<OIDCAuthProvider> getAvailableOidcProviders() {
.map(providerId -> (OIDCAuthProvider) getAuthenticationProvider(providerId))
.toList();
}
}

public OrcidOAuth2AP getOrcidAuthenticationProvider() {
return (OrcidOAuth2AP) authProvidersRegistrationService.getOrcidProvider();
}

public AuthenticatedUser lookupUserByOrcid(String orcid) {
if (orcid == null || orcid.isEmpty()) {
return null;
}

try {
TypedQuery<AuthenticatedUser> query = em.createQuery(
"SELECT au FROM AuthenticatedUser au WHERE au.authenticatedOrcid = :orcid",
AuthenticatedUser.class);
query.setParameter("orcid", orcid);
return query.getSingleResult();
} catch (NoResultException e) {
return null;
} catch (NonUniqueResultException e) {
logger.log(Level.WARNING, "Multiple users found with ORCID: " + orcid, e);
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class AuthenticationProviderRow implements java.io.Serializable {

private String factoryAlias;

//Enabled for login (and possibly for registration depending on the :AllowRemoteAuthSignUp setting)
private boolean enabled;

@Lob
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,4 @@ public boolean isOAuthProvider() {
return false;
}

@Override
public boolean isDisplayIdentifier() {
return false;
}

}
Loading