Skip to content

Commit ab2c661

Browse files
authored
Merge pull request #11671 from IQSS/11645-old-oidc-users-api-auth
Existing OAuth users API OIDC Authentication
2 parents 92d1ec8 + 5cca889 commit ab2c661

File tree

14 files changed

+287
-20
lines changed

14 files changed

+287
-20
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Implemented a new feature flag ``dataverse.feature.api-bearer-auth-use-oauth-user-on-id-match``, which supports the use of the new Dataverse client in instances that have historically allowed login via GitHub, ORCID, or Google. Specifically, with this flag enabled, when an OIDC bridge is configured to allow OIDC login with validation by the bridged OAuth providers, users with existing GitHub, ORCID, or Google accounts in Dataverse can log in to those accounts, thereby maintaining access to their existing content and retaining their roles.
2+
3+
## New Settings
4+
5+
- dataverse.feature.api-bearer-auth-use-oauth-user-on-id-match
6+

doc/sphinx-guides/source/installation/config.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3755,6 +3755,9 @@ please find all known feature flags below. Any of these flags can be activated u
37553755
* - api-bearer-auth-use-shib-user-on-id-match
37563756
- Allows the use of a Shibboleth user account when an identity match is found during API bearer authentication. This feature enables automatic association of an incoming IdP identity with an existing Shibboleth user account, bypassing the need for additional user registration steps. This feature only works when the feature flag ``api-bearer-auth`` is also enabled. **Caution: Enabling this flag could result in impersonation risks if (and only if) used with a misconfigured IdP.**
37573757
- ``Off``
3758+
* - api-bearer-auth-use-oauth-user-on-id-match
3759+
- Allows the use of an OAuth user account (GitHub, Google, or ORCID) when an identity match is found during API bearer authentication. This feature enables automatic association of an incoming IdP identity with an existing OAuth user account, bypassing the need for additional user registration steps. This feature only works when the feature flag ``api-bearer-auth`` is also enabled. **Caution: Enabling this flag could result in impersonation risks if (and only if) used with a misconfigured IdP.**
3760+
- ``Off``
37583761
* - avoid-expensive-solr-join
37593762
- Changes the way Solr queries are constructed for public content (published Collections, Datasets and Files). It removes a very expensive Solr join on all such documents, improving overall performance, especially for large instances under heavy load. Before this feature flag is enabled, the corresponding indexing feature (see next feature flag) must be turned on and a full reindex performed (otherwise public objects are not going to be shown in search results). See :doc:`/admin/solr-search-index`.
37603763
- ``Off``

src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -999,12 +999,19 @@ public AuthenticatedUser lookupUserByOIDCBearerToken(String bearerToken) throws
999999
AuthenticatedUser authenticatedUser;
10001000
if (FeatureFlags.API_BEARER_AUTH_USE_SHIB_USER_ON_ID_MATCH.enabled() && oAuth2UserRecord.hasShibAttributes()) {
10011001
logger.log(Level.FINE, "OAuth2UserRecord has Shibboleth attributes");
1002-
String userPersistentId = ShibUtil.createUserPersistentIdentifier(oAuth2UserRecord.getShibIdp(), oAuth2UserRecord.getShibUniquePersistentIdentifier());
1002+
String userPersistentId = ShibUtil.createUserPersistentIdentifier(oAuth2UserRecord.getIdp(), oAuth2UserRecord.getShibUniquePersistentIdentifier());
10031003
authenticatedUser = lookupUser(ShibAuthenticationProvider.PROVIDER_ID, userPersistentId);
10041004
if (authenticatedUser != null) {
10051005
logger.log(Level.FINE, "Shibboleth user found for the given bearer token");
10061006
return authenticatedUser;
10071007
}
1008+
} else if (FeatureFlags.API_BEARER_AUTH_USE_OAUTH_USER_ON_ID_MATCH.enabled() && oAuth2UserRecord.hasOAuthAttributes()) {
1009+
OAuthUserLookupParams userLookupParams = OAuthUserLookupParamsFactory.getOAuthUserLookupParams(oAuth2UserRecord.getIdp(), oAuth2UserRecord.getOidcUserId());
1010+
authenticatedUser = lookupUser(userLookupParams.getProviderId(), userLookupParams.getLookupUserId());
1011+
if (authenticatedUser != null) {
1012+
logger.log(Level.FINE, "OAuth user found for the given bearer token");
1013+
return authenticatedUser;
1014+
}
10081015
} else if (FeatureFlags.API_BEARER_AUTH_USE_BUILTIN_USER_ON_ID_MATCH.enabled()) {
10091016
authenticatedUser = lookupUser(BuiltinAuthenticationProvider.PROVIDER_ID, oAuth2UserRecord.getUsername());
10101017
if (authenticatedUser != null) {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package edu.harvard.iq.dataverse.authorization;
2+
3+
import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.GitHubOAuth2AP;
4+
5+
public class GitHubUserLookupParams extends OAuthUserLookupParams {
6+
7+
public GitHubUserLookupParams(String userId) {
8+
super(userId);
9+
}
10+
11+
@Override
12+
public String getProviderId() {
13+
return GitHubOAuth2AP.PROVIDER_ID;
14+
}
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package edu.harvard.iq.dataverse.authorization;
2+
3+
import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.GoogleOAuth2AP;
4+
5+
public class GoogleUserLookupParams extends OAuthUserLookupParams {
6+
7+
public GoogleUserLookupParams(String userId) {
8+
super(userId);
9+
}
10+
11+
@Override
12+
public String getProviderId() {
13+
return GoogleOAuth2AP.PROVIDER_ID;
14+
}
15+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package edu.harvard.iq.dataverse.authorization;
2+
3+
abstract class OAuthUserLookupParams {
4+
5+
protected String userId;
6+
7+
public OAuthUserLookupParams(String userId) {
8+
this.userId = userId;
9+
}
10+
11+
public String getLookupUserId() {
12+
return userId;
13+
}
14+
15+
public abstract String getProviderId();
16+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package edu.harvard.iq.dataverse.authorization;
2+
3+
import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.GitHubOAuth2AP;
4+
import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.GoogleOAuth2AP;
5+
import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.OrcidOAuth2AP;
6+
7+
import java.util.Map;
8+
import java.util.function.Function;
9+
10+
/**
11+
* A factory for creating {@link OAuthUserLookupParams} instances based on an identity provider.
12+
* This is a non-instantiable utility class.
13+
*/
14+
public final class OAuthUserLookupParamsFactory {
15+
16+
/**
17+
* A map linking provider IDs to their corresponding user searcher constructor.
18+
*/
19+
private static final Map<String, Function<String, OAuthUserLookupParams>> PROVIDER_MAP = Map.of(
20+
GoogleOAuth2AP.PROVIDER_ID, GoogleUserLookupParams::new,
21+
GitHubOAuth2AP.PROVIDER_ID, GitHubUserLookupParams::new,
22+
OrcidOAuth2AP.PROVIDER_ID, ORCIDUserLookupParams::new
23+
);
24+
25+
private OAuthUserLookupParamsFactory() {
26+
// Prevent instantiation of this utility class.
27+
}
28+
29+
/**
30+
* Creates an instance of an {@link OAuthUserLookupParams} based on the identity provider claim.
31+
*
32+
* @param idpClaim The identity provider claim value (e.g., "https://accounts.google.com").
33+
* @param userId The user identifier from the OAuth provider.
34+
* @return A new instance of a concrete {@link OAuthUserLookupParams}.
35+
* @throws IllegalArgumentException if the identity provider is not supported.
36+
*/
37+
public static OAuthUserLookupParams getOAuthUserLookupParams(String idpClaim, String userId) {
38+
return PROVIDER_MAP.keySet().stream()
39+
.filter(idpClaim::contains)
40+
.findFirst()
41+
.map(providerId -> PROVIDER_MAP.get(providerId).apply(userId))
42+
.orElseThrow(() -> new IllegalArgumentException("Unsupported OAuth provider: " + idpClaim));
43+
}
44+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package edu.harvard.iq.dataverse.authorization;
2+
3+
import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.OrcidOAuth2AP;
4+
5+
public class ORCIDUserLookupParams extends OAuthUserLookupParams {
6+
7+
private static final String ORCID_BASE_URL = "http://orcid.org/";
8+
private static final String ORCID_BASE_URL_HTTPS = "https://orcid.org/";
9+
10+
public ORCIDUserLookupParams(String userId) {
11+
super(userId);
12+
}
13+
14+
@Override
15+
public String getLookupUserId() {
16+
return extractIdFromUrl(userId);
17+
}
18+
19+
@Override
20+
public String getProviderId() {
21+
return OrcidOAuth2AP.PROVIDER_ID;
22+
}
23+
24+
/**
25+
* Extracts the ORCID iD from a full ORCID URL.
26+
* <p>
27+
* This method checks if the provided string starts with "http://orcid.org/" or "https://orcid.org/"
28+
* and, if so, returns the trailing part of the string. If the string does not
29+
* match the base URL, it is returned as-is, assuming it might already be the ID.
30+
*
31+
* @param orcidUrlOrId The full ORCID URL (e.g., "http://orcid.org/0009-0007-1267-8782")
32+
* or an ORCID iD itself.
33+
* @return The extracted ORCID iD (e.g., "0009-0007-1267-8782"), or the original string if it's not a URL.
34+
* Returns null if the input is null.
35+
*/
36+
private static String extractIdFromUrl(String orcidUrlOrId) {
37+
if (orcidUrlOrId == null) {
38+
return null;
39+
}
40+
if (orcidUrlOrId.startsWith(ORCID_BASE_URL)) {
41+
return orcidUrlOrId.substring(ORCID_BASE_URL.length());
42+
}
43+
if (orcidUrlOrId.startsWith(ORCID_BASE_URL_HTTPS)) {
44+
return orcidUrlOrId.substring(ORCID_BASE_URL_HTTPS.length());
45+
}
46+
// If it's not a URL, assume it's already the ID.
47+
return orcidUrlOrId;
48+
}
49+
}

src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2UserRecord.java

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414
*/
1515
public class OAuth2UserRecord implements Serializable {
1616

17+
/**
18+
* The following claim names are expected to be received when using the
19+
* CILogon org.cilogon.userinfo scope specification. For more details, see
20+
* https://www.cilogon.org/oidc
21+
*/
22+
public static final String OIDC_USER_ID_CLAIM_NAME = "oidc";
23+
public static final String IDP_CLAIM_NAME = "idp";
24+
1725
private final String serviceId;
1826

1927
/**
@@ -30,8 +38,13 @@ public class OAuth2UserRecord implements Serializable {
3038
* For users originally coming from a Shibboleth IdP
3139
*/
3240
private final String shibUniquePersistentIdentifier;
33-
private final String shibIdp;
3441

42+
/**
43+
* For brokered users coming from another OIDC provider
44+
*/
45+
private final String oidcUserId;
46+
47+
private final String idp;
3548
private final AuthenticatedUserDisplayInfo displayInfo;
3649
private final List<String> availableEmailAddresses;
3750
private final OAuth2TokenData tokenData;
@@ -47,7 +60,7 @@ public OAuth2UserRecord(
4760
AuthenticatedUserDisplayInfo displayInfo,
4861
List<String> availableEmailAddresses
4962
) {
50-
this(serviceId, idInService, username, null, null, tokenData, displayInfo, availableEmailAddresses);
63+
this(serviceId, idInService, username, null, null, null, tokenData, displayInfo, availableEmailAddresses);
5164
}
5265

5366
/**
@@ -58,7 +71,8 @@ public OAuth2UserRecord(
5871
String idInService,
5972
String username,
6073
String shibUniquePersistentIdentifier,
61-
String shibIdp,
74+
String idp,
75+
String oidcUserId,
6276
OAuth2TokenData tokenData,
6377
AuthenticatedUserDisplayInfo displayInfo,
6478
List<String> availableEmailAddresses
@@ -67,7 +81,8 @@ public OAuth2UserRecord(
6781
this.idInService = idInService;
6882
this.username = username;
6983
this.shibUniquePersistentIdentifier = shibUniquePersistentIdentifier;
70-
this.shibIdp = shibIdp;
84+
this.idp = idp;
85+
this.oidcUserId = oidcUserId;
7186
this.tokenData = tokenData;
7287
this.displayInfo = displayInfo;
7388
this.availableEmailAddresses = availableEmailAddresses;
@@ -89,8 +104,12 @@ public String getShibUniquePersistentIdentifier() {
89104
return shibUniquePersistentIdentifier;
90105
}
91106

92-
public String getShibIdp() {
93-
return shibIdp;
107+
public String getIdp() {
108+
return idp;
109+
}
110+
111+
public String getOidcUserId() {
112+
return oidcUserId;
94113
}
95114

96115
public List<String> getAvailableEmailAddresses() {
@@ -110,7 +129,11 @@ public UserRecordIdentifier getUserRecordIdentifier() {
110129
}
111130

112131
public boolean hasShibAttributes() {
113-
return shibIdp != null && shibUniquePersistentIdentifier != null;
132+
return idp != null && shibUniquePersistentIdentifier != null;
133+
}
134+
135+
public boolean hasOAuthAttributes() {
136+
return idp != null && oidcUserId != null;
114137
}
115138

116139
@Override

src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/impl/GitHubOAuth2AP.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
* @author michael
1919
*/
2020
public class GitHubOAuth2AP extends AbstractOAuth2AuthenticationProvider {
21-
21+
22+
public static final String PROVIDER_ID = "github";
23+
2224
public GitHubOAuth2AP(String aClientId, String aClientSecret) {
23-
id = "github";
25+
id = PROVIDER_ID;
2426
title = BundleUtil.getStringFromBundle("auth.providers.title.github");
2527
clientId = aClientId;
2628
clientSecret = aClientSecret;

0 commit comments

Comments
 (0)