diff --git a/doc/release-notes/11645-existing-oauth-external-users-api-auth.md b/doc/release-notes/11645-existing-oauth-external-users-api-auth.md new file mode 100644 index 00000000000..4afc5a38fb2 --- /dev/null +++ b/doc/release-notes/11645-existing-oauth-external-users-api-auth.md @@ -0,0 +1,6 @@ +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. + +## New Settings + +- dataverse.feature.api-bearer-auth-use-oauth-user-on-id-match + diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index c2840f3456f..0fed3ca5ae7 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -3729,6 +3729,9 @@ please find all known feature flags below. Any of these flags can be activated u * - api-bearer-auth-use-shib-user-on-id-match - 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.** - ``Off`` + * - api-bearer-auth-use-oauth-user-on-id-match + - 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.** + - ``Off`` * - avoid-expensive-solr-join - 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`. - ``Off`` diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java index b43bc0e0bb0..891eb62fd97 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -999,12 +999,19 @@ public AuthenticatedUser lookupUserByOIDCBearerToken(String bearerToken) throws AuthenticatedUser authenticatedUser; if (FeatureFlags.API_BEARER_AUTH_USE_SHIB_USER_ON_ID_MATCH.enabled() && oAuth2UserRecord.hasShibAttributes()) { logger.log(Level.FINE, "OAuth2UserRecord has Shibboleth attributes"); - String userPersistentId = ShibUtil.createUserPersistentIdentifier(oAuth2UserRecord.getShibIdp(), oAuth2UserRecord.getShibUniquePersistentIdentifier()); + String userPersistentId = ShibUtil.createUserPersistentIdentifier(oAuth2UserRecord.getIdp(), oAuth2UserRecord.getShibUniquePersistentIdentifier()); authenticatedUser = lookupUser(ShibAuthenticationProvider.PROVIDER_ID, userPersistentId); if (authenticatedUser != null) { logger.log(Level.FINE, "Shibboleth user found for the given bearer token"); return authenticatedUser; } + } else if (FeatureFlags.API_BEARER_AUTH_USE_OAUTH_USER_ON_ID_MATCH.enabled() && oAuth2UserRecord.hasOAuthAttributes()) { + OAuthUserLookupParams userLookupParams = OAuthUserLookupParamsFactory.getOAuthUserLookupParams(oAuth2UserRecord.getIdp(), oAuth2UserRecord.getOidcUserId()); + authenticatedUser = lookupUser(userLookupParams.getProviderId(), userLookupParams.getLookupUserId()); + if (authenticatedUser != null) { + logger.log(Level.FINE, "OAuth user found for the given bearer token"); + return authenticatedUser; + } } else if (FeatureFlags.API_BEARER_AUTH_USE_BUILTIN_USER_ON_ID_MATCH.enabled()) { authenticatedUser = lookupUser(BuiltinAuthenticationProvider.PROVIDER_ID, oAuth2UserRecord.getUsername()); if (authenticatedUser != null) { diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/GitHubUserLookupParams.java b/src/main/java/edu/harvard/iq/dataverse/authorization/GitHubUserLookupParams.java new file mode 100644 index 00000000000..0296a1fa730 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/GitHubUserLookupParams.java @@ -0,0 +1,15 @@ +package edu.harvard.iq.dataverse.authorization; + +import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.GitHubOAuth2AP; + +public class GitHubUserLookupParams extends OAuthUserLookupParams { + + public GitHubUserLookupParams(String userId) { + super(userId); + } + + @Override + public String getProviderId() { + return GitHubOAuth2AP.PROVIDER_ID; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/GoogleUserLookupParams.java b/src/main/java/edu/harvard/iq/dataverse/authorization/GoogleUserLookupParams.java new file mode 100644 index 00000000000..be3762a3e44 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/GoogleUserLookupParams.java @@ -0,0 +1,15 @@ +package edu.harvard.iq.dataverse.authorization; + +import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.GoogleOAuth2AP; + +public class GoogleUserLookupParams extends OAuthUserLookupParams { + + public GoogleUserLookupParams(String userId) { + super(userId); + } + + @Override + public String getProviderId() { + return GoogleOAuth2AP.PROVIDER_ID; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/OAuthUserLookupParams.java b/src/main/java/edu/harvard/iq/dataverse/authorization/OAuthUserLookupParams.java new file mode 100644 index 00000000000..60a8c7f5377 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/OAuthUserLookupParams.java @@ -0,0 +1,16 @@ +package edu.harvard.iq.dataverse.authorization; + +abstract class OAuthUserLookupParams { + + protected String userId; + + public OAuthUserLookupParams(String userId) { + this.userId = userId; + } + + public String getLookupUserId() { + return userId; + } + + public abstract String getProviderId(); +} diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/OAuthUserLookupParamsFactory.java b/src/main/java/edu/harvard/iq/dataverse/authorization/OAuthUserLookupParamsFactory.java new file mode 100644 index 00000000000..e9a5aff6d48 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/OAuthUserLookupParamsFactory.java @@ -0,0 +1,44 @@ +package edu.harvard.iq.dataverse.authorization; + +import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.GitHubOAuth2AP; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.GoogleOAuth2AP; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.OrcidOAuth2AP; + +import java.util.Map; +import java.util.function.Function; + +/** + * A factory for creating {@link OAuthUserLookupParams} instances based on an identity provider. + * This is a non-instantiable utility class. + */ +public final class OAuthUserLookupParamsFactory { + + /** + * A map linking provider IDs to their corresponding user searcher constructor. + */ + private static final Map> PROVIDER_MAP = Map.of( + GoogleOAuth2AP.PROVIDER_ID, GoogleUserLookupParams::new, + GitHubOAuth2AP.PROVIDER_ID, GitHubUserLookupParams::new, + OrcidOAuth2AP.PROVIDER_ID, ORCIDUserLookupParams::new + ); + + private OAuthUserLookupParamsFactory() { + // Prevent instantiation of this utility class. + } + + /** + * Creates an instance of an {@link OAuthUserLookupParams} based on the identity provider claim. + * + * @param idpClaim The identity provider claim value (e.g., "https://accounts.google.com"). + * @param userId The user identifier from the OAuth provider. + * @return A new instance of a concrete {@link OAuthUserLookupParams}. + * @throws IllegalArgumentException if the identity provider is not supported. + */ + public static OAuthUserLookupParams getOAuthUserLookupParams(String idpClaim, String userId) { + return PROVIDER_MAP.keySet().stream() + .filter(idpClaim::contains) + .findFirst() + .map(providerId -> PROVIDER_MAP.get(providerId).apply(userId)) + .orElseThrow(() -> new IllegalArgumentException("Unsupported OAuth provider: " + idpClaim)); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/ORCIDUserLookupParams.java b/src/main/java/edu/harvard/iq/dataverse/authorization/ORCIDUserLookupParams.java new file mode 100644 index 00000000000..34517bb080a --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/ORCIDUserLookupParams.java @@ -0,0 +1,49 @@ +package edu.harvard.iq.dataverse.authorization; + +import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.OrcidOAuth2AP; + +public class ORCIDUserLookupParams extends OAuthUserLookupParams { + + private static final String ORCID_BASE_URL = "http://orcid.org/"; + private static final String ORCID_BASE_URL_HTTPS = "https://orcid.org/"; + + public ORCIDUserLookupParams(String userId) { + super(userId); + } + + @Override + public String getLookupUserId() { + return extractIdFromUrl(userId); + } + + @Override + public String getProviderId() { + return OrcidOAuth2AP.PROVIDER_ID; + } + + /** + * Extracts the ORCID iD from a full ORCID URL. + *

+ * This method checks if the provided string starts with "http://orcid.org/" or "https://orcid.org/" + * and, if so, returns the trailing part of the string. If the string does not + * match the base URL, it is returned as-is, assuming it might already be the ID. + * + * @param orcidUrlOrId The full ORCID URL (e.g., "http://orcid.org/0009-0007-1267-8782") + * or an ORCID iD itself. + * @return The extracted ORCID iD (e.g., "0009-0007-1267-8782"), or the original string if it's not a URL. + * Returns null if the input is null. + */ + private static String extractIdFromUrl(String orcidUrlOrId) { + if (orcidUrlOrId == null) { + return null; + } + if (orcidUrlOrId.startsWith(ORCID_BASE_URL)) { + return orcidUrlOrId.substring(ORCID_BASE_URL.length()); + } + if (orcidUrlOrId.startsWith(ORCID_BASE_URL_HTTPS)) { + return orcidUrlOrId.substring(ORCID_BASE_URL_HTTPS.length()); + } + // If it's not a URL, assume it's already the ID. + return orcidUrlOrId; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2UserRecord.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2UserRecord.java index 3f75e882d82..8b9857af825 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2UserRecord.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2UserRecord.java @@ -14,6 +14,14 @@ */ public class OAuth2UserRecord implements Serializable { + /** + * The following claim names are expected to be received when using the + * CILogon org.cilogon.userinfo scope specification. For more details, see + * https://www.cilogon.org/oidc + */ + public static final String OIDC_USER_ID_CLAIM_NAME = "oidc"; + public static final String IDP_CLAIM_NAME = "idp"; + private final String serviceId; /** @@ -30,8 +38,13 @@ public class OAuth2UserRecord implements Serializable { * For users originally coming from a Shibboleth IdP */ private final String shibUniquePersistentIdentifier; - private final String shibIdp; + /** + * For brokered users coming from another OIDC provider + */ + private final String oidcUserId; + + private final String idp; private final AuthenticatedUserDisplayInfo displayInfo; private final List availableEmailAddresses; private final OAuth2TokenData tokenData; @@ -47,7 +60,7 @@ public OAuth2UserRecord( AuthenticatedUserDisplayInfo displayInfo, List availableEmailAddresses ) { - this(serviceId, idInService, username, null, null, tokenData, displayInfo, availableEmailAddresses); + this(serviceId, idInService, username, null, null, null, tokenData, displayInfo, availableEmailAddresses); } /** @@ -58,7 +71,8 @@ public OAuth2UserRecord( String idInService, String username, String shibUniquePersistentIdentifier, - String shibIdp, + String idp, + String oidcUserId, OAuth2TokenData tokenData, AuthenticatedUserDisplayInfo displayInfo, List availableEmailAddresses @@ -67,7 +81,8 @@ public OAuth2UserRecord( this.idInService = idInService; this.username = username; this.shibUniquePersistentIdentifier = shibUniquePersistentIdentifier; - this.shibIdp = shibIdp; + this.idp = idp; + this.oidcUserId = oidcUserId; this.tokenData = tokenData; this.displayInfo = displayInfo; this.availableEmailAddresses = availableEmailAddresses; @@ -89,8 +104,12 @@ public String getShibUniquePersistentIdentifier() { return shibUniquePersistentIdentifier; } - public String getShibIdp() { - return shibIdp; + public String getIdp() { + return idp; + } + + public String getOidcUserId() { + return oidcUserId; } public List getAvailableEmailAddresses() { @@ -110,7 +129,11 @@ public UserRecordIdentifier getUserRecordIdentifier() { } public boolean hasShibAttributes() { - return shibIdp != null && shibUniquePersistentIdentifier != null; + return idp != null && shibUniquePersistentIdentifier != null; + } + + public boolean hasOAuthAttributes() { + return idp != null && oidcUserId != null; } @Override diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/impl/GitHubOAuth2AP.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/impl/GitHubOAuth2AP.java index 5b201ce77db..457a91bc932 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/impl/GitHubOAuth2AP.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/impl/GitHubOAuth2AP.java @@ -18,9 +18,11 @@ * @author michael */ public class GitHubOAuth2AP extends AbstractOAuth2AuthenticationProvider { - + + public static final String PROVIDER_ID = "github"; + public GitHubOAuth2AP(String aClientId, String aClientSecret) { - id = "github"; + id = PROVIDER_ID; title = BundleUtil.getStringFromBundle("auth.providers.title.github"); clientId = aClientId; clientSecret = aClientSecret; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/impl/GoogleOAuth2AP.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/impl/GoogleOAuth2AP.java index 913eb038d8e..a0889a5bfd3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/impl/GoogleOAuth2AP.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/impl/GoogleOAuth2AP.java @@ -17,9 +17,11 @@ * @author michael */ public class GoogleOAuth2AP extends AbstractOAuth2AuthenticationProvider { - + + public static final String PROVIDER_ID = "google"; + public GoogleOAuth2AP(String aClientId, String aClientSecret) { - id = "google"; + id = PROVIDER_ID; title = BundleUtil.getStringFromBundle("auth.providers.title.google"); clientId = aClientId; clientSecret = aClientSecret; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java index 4085777f180..3e62598ee79 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java @@ -228,12 +228,18 @@ public OAuth2UserRecord getUserRecord(String code, String state, String redirect * @return the usable user record for processing ing {@link edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2LoginBackingBean} */ public OAuth2UserRecord getUserRecord(UserInfo userInfo) { - // Extract Shibboleth attributes if present + // Extract Shibboleth persistent identifier claim if present Object shibUniqueIdObj = userInfo.getClaim(ShibUtil.uniquePersistentIdentifier); - Object shibIdpObj = userInfo.getClaim(ShibUtil.shibIdpAttribute); + + // Extract idp claim if present + Object idpObj = userInfo.getClaim(OAuth2UserRecord.IDP_CLAIM_NAME); + + // Extract OIDC user id claim if present + Object oidcUserIdObj = userInfo.getClaim(OAuth2UserRecord.OIDC_USER_ID_CLAIM_NAME); String shibUniqueId = (shibUniqueIdObj != null) ? shibUniqueIdObj.toString() : null; - String shibIdp = (shibIdpObj != null) ? shibIdpObj.toString() : null; + String idp = (idpObj != null) ? idpObj.toString() : null; + String oidcUserId = (oidcUserIdObj != null) ? oidcUserIdObj.toString() : null; // Build display info from user attributes AuthenticatedUserDisplayInfo displayInfo = new AuthenticatedUserDisplayInfo( @@ -249,7 +255,8 @@ public OAuth2UserRecord getUserRecord(UserInfo userInfo) { userInfo.getSubject().getValue(), userInfo.getPreferredUsername(), shibUniqueId, - shibIdp, + idp, + oidcUserId, null, displayInfo, null diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java index b0aaae7b0d4..bbbbc7aee30 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java @@ -85,6 +85,19 @@ public enum FeatureFlags { */ API_BEARER_AUTH_USE_SHIB_USER_ON_ID_MATCH("api-bearer-auth-use-shib-user-on-id-match"), + /** + * 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. + * + *

The value of this feature flag is only considered when the feature flag + * {@link #API_BEARER_AUTH} is enabled.

+ * + * @apiNote Raise flag by setting "dataverse.feature.api-bearer-auth-use-oauth-user-on-id-match" + * @since Dataverse 6.8: + */ + API_BEARER_AUTH_USE_OAUTH_USER_ON_ID_MATCH("api-bearer-auth-use-oauth-user-on-id-match"), + /** * For published (public) objects, don't use a join when searching Solr. * Experimental! Requires a reindex with the following feature flag enabled, diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java index 853cfece280..80b1c99c642 100644 --- a/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java @@ -7,6 +7,9 @@ import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinAuthenticationProvider; 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.GitHubOAuth2AP; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.GoogleOAuth2AP; +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.authorization.providers.shib.ShibAuthenticationProvider; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; @@ -20,12 +23,16 @@ import jakarta.persistence.TypedQuery; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import java.io.IOException; import java.util.Map; import java.util.Optional; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.*; @@ -34,6 +41,9 @@ public class AuthenticationServiceBeanTest { private AuthenticationServiceBean sut; private static final String TEST_BEARER_TOKEN = "Bearer test"; + private static final String TEST_ORCID_USER_ID = "0000-0000-0000-0000"; + private static final String TEST_GOOGLE_USER_ID = "111111111111111111111"; + private static final String TEST_GITHUB_USER_ID = "11111111"; @BeforeEach public void setUp() { @@ -181,7 +191,7 @@ void testLookupUserByOIDCBearerToken_oneProvider_validToken_userIsPresentAsBuilt @JvmSetting(key = JvmSettings.FEATURE_FLAG, value = "true", varArgs = "api-bearer-auth-use-shib-user-on-id-match") void testLookupUserByOIDCBearerToken_oneProvider_validToken_userIsPresentAsShibboleth_useShibUserOnIdMatchFeatureFlagEnabled() throws ParseException, IOException, AuthorizationException, OAuth2Exception { // Given a single OIDC provider that returns a valid user identifier - setUpOIDCProviderWhichValidatesToken(true); + setUpOIDCProviderWhichValidatesToken(true, null); // Spy on the SUT to verify method calls AuthenticationServiceBean spySut = Mockito.spy(sut); @@ -207,6 +217,45 @@ void testLookupUserByOIDCBearerToken_oneProvider_validToken_userIsPresentAsShibb assertEquals("testIdp|testPersistentId", userIdCaptor.getAllValues().get(0)); } + private static Stream oAuthProvider() { + return Stream.of( + Arguments.of(OrcidOAuth2AP.PROVIDER_ID, TEST_ORCID_USER_ID), + Arguments.of(GoogleOAuth2AP.PROVIDER_ID, TEST_GOOGLE_USER_ID), + Arguments.of(GitHubOAuth2AP.PROVIDER_ID, TEST_GITHUB_USER_ID) + ); + } + + @ParameterizedTest + @MethodSource("oAuthProvider") + @JvmSetting(key = JvmSettings.FEATURE_FLAG, value = "true", varArgs = "api-bearer-auth-use-oauth-user-on-id-match") + void testLookupUserByOIDCBearerToken_oneProvider_validToken_userIsPresentAsOAuth_useOAuthUserOnIdMatchFeatureFlagEnabled(String providerId, String expectedUserId) throws ParseException, IOException, AuthorizationException, OAuth2Exception { + // Given a single OIDC provider that returns a valid user identifier with OAuth attributes + setUpOIDCProviderWhichValidatesToken(false, providerId); + + // Spy on the SUT to verify method calls + AuthenticationServiceBean spySut = Mockito.spy(sut); + + // Setting up an authenticated user is found + AuthenticatedUser authenticatedUser = setupAuthenticatedUserByAuthPrvIDQueryWithResult(new AuthenticatedUser()); + + // When invoking lookupUserByOIDCBearerToken + User actualUser = spySut.lookupUserByOIDCBearerToken(TEST_BEARER_TOKEN); + + // Then the actual user should match the expected authenticated user + assertEquals(authenticatedUser, actualUser); + + // Capture calls to lookupUser + ArgumentCaptor providerIdCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor userIdCaptor = ArgumentCaptor.forClass(String.class); + + // Ensure lookupUser is called once + Mockito.verify(spySut, Mockito.times(1)).lookupUser(providerIdCaptor.capture(), userIdCaptor.capture()); + + // Assert that lookupUser is called with expected parameters + assertEquals(providerId, providerIdCaptor.getAllValues().get(0)); + assertEquals(expectedUserId, userIdCaptor.getAllValues().get(0)); + } + private void setupAuthenticatedUserQueryWithNoResult() { TypedQuery queryStub = Mockito.mock(TypedQuery.class); Mockito.when(queryStub.getSingleResult()).thenThrow(new NoResultException()); @@ -214,10 +263,10 @@ private void setupAuthenticatedUserQueryWithNoResult() { } private void setUpOIDCProviderWhichValidatesToken() throws ParseException, IOException, OAuth2Exception { - setUpOIDCProviderWhichValidatesToken(false); + setUpOIDCProviderWhichValidatesToken(false, null); } - private void setUpOIDCProviderWhichValidatesToken(boolean includeShibAttributes) throws ParseException, IOException, OAuth2Exception { + private void setUpOIDCProviderWhichValidatesToken(boolean includeShibAttributes, String providerIdToIncludeClaimsFor) throws ParseException, IOException, OAuth2Exception { OIDCAuthProvider oidcAuthProviderStub = stubOIDCAuthProvider("OIDC"); BearerAccessToken token = BearerAccessToken.parse(TEST_BEARER_TOKEN); @@ -231,8 +280,24 @@ private void setUpOIDCProviderWhichValidatesToken(boolean includeShibAttributes) if (includeShibAttributes) { Mockito.when(oAuth2UserRecordStub.hasShibAttributes()).thenReturn(true); - Mockito.when(oAuth2UserRecordStub.getShibIdp()).thenReturn("testIdp"); + Mockito.when(oAuth2UserRecordStub.getIdp()).thenReturn("testIdp"); Mockito.when(oAuth2UserRecordStub.getShibUniquePersistentIdentifier()).thenReturn("testPersistentId"); + } else if (providerIdToIncludeClaimsFor != null) { + Mockito.when(oAuth2UserRecordStub.hasOAuthAttributes()).thenReturn(true); + switch (providerIdToIncludeClaimsFor) { + case OrcidOAuth2AP.PROVIDER_ID -> { + Mockito.when(oAuth2UserRecordStub.getIdp()).thenReturn("http://orcid.org/oauth/authorize"); + Mockito.when(oAuth2UserRecordStub.getOidcUserId()).thenReturn("http://orcid.org/" + TEST_ORCID_USER_ID); + } + case GoogleOAuth2AP.PROVIDER_ID -> { + Mockito.when(oAuth2UserRecordStub.getIdp()).thenReturn("http://google.com/accounts/o8/id"); + Mockito.when(oAuth2UserRecordStub.getOidcUserId()).thenReturn(TEST_GOOGLE_USER_ID); + } + case GitHubOAuth2AP.PROVIDER_ID -> { + Mockito.when(oAuth2UserRecordStub.getIdp()).thenReturn("http://github.com/login/oauth/authorize"); + Mockito.when(oAuth2UserRecordStub.getOidcUserId()).thenReturn(TEST_GITHUB_USER_ID); + } + } } UserRecordIdentifier userRecordIdentifierStub = Mockito.mock(UserRecordIdentifier.class);