diff --git a/pom.xml b/pom.xml index 229f200c198..c000cd6740d 100644 --- a/pom.xml +++ b/pom.xml @@ -31,7 +31,7 @@ 5.3.1 5.3.1 1.3.1 - 2.22.0 + 2.28.2 5.2.4 1.20.1 diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProvider.java index 8cfb84e7ce3..d26a87cf020 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProvider.java @@ -1,7 +1,7 @@ package edu.harvard.iq.dataverse.authorization.providers.oauth2; import com.github.scribejava.core.builder.ServiceBuilder; -import com.github.scribejava.core.builder.api.BaseApi; +import com.github.scribejava.core.builder.api.DefaultApi20; import com.github.scribejava.core.model.OAuth2AccessToken; import com.github.scribejava.core.model.OAuthRequest; import com.github.scribejava.core.model.Response; @@ -10,12 +10,12 @@ import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; import edu.harvard.iq.dataverse.authorization.AuthenticationProvider; import edu.harvard.iq.dataverse.authorization.AuthenticationProviderDisplayInfo; +import edu.harvard.iq.dataverse.util.BundleUtil; + +import javax.validation.constraints.NotNull; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; +import java.util.*; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; @@ -89,50 +89,88 @@ public String toString() { protected String clientSecret; protected String baseUserEndpoint; protected String redirectUrl; - protected String scope; + /** + * List of scopes to be requested for authorization at identity provider. + * Defaults to empty so no scope will be requested (use case: public info from GitHub) + */ + protected List scope = Arrays.asList(""); - public abstract BaseApi getApiInstance(); + public abstract DefaultApi20 getApiInstance(); protected abstract ParsedUserResponse parseUserResponse( String responseBody ); - public OAuth20Service getService(String state, String redirectUrl) { - ServiceBuilder svcBuilder = new ServiceBuilder() - .apiKey(getClientId()) - .apiSecret(getClientSecret()) - .state(state) - .callback(redirectUrl); - if ( scope != null ) { - svcBuilder.scope(scope); - } - return svcBuilder.build( getApiInstance() ); + /** + * Build an OAuth20Service based on client ID & secret. Add default scope and insert + * callback URL. Build uses the real API object for the target service like GitHub etc. + * @param callbackUrl URL where the OAuth2 Provider should send browsers to after authz. + * @return A usable OAuth20Service object + */ + public OAuth20Service getService(String callbackUrl) { + return new ServiceBuilder(getClientId()) + .apiSecret(getClientSecret()) + .callback(callbackUrl) + .build(getApiInstance()); } - public OAuth2UserRecord getUserRecord(String code, String state, String redirectUrl) throws IOException, OAuth2Exception { - OAuth20Service service = getService(state, redirectUrl); + /** + * Receive user data from OAuth2 provider after authn/z has been successfull. (Callback view uses this) + * Request a token and access the resource, parse output and return user details. + * @param code The authz code sent from the provider + * @param service The service object in use to communicate with the provider + * @return A user record containing all user details accessible for us + * @throws IOException Thrown when communication with the provider fails + * @throws OAuth2Exception Thrown when we cannot access the user details for some reason + * @throws InterruptedException Thrown when the requests thread is failing + * @throws ExecutionException Thrown when the requests thread is failing + */ + public OAuth2UserRecord getUserRecord(String code, @NotNull OAuth20Service service) + throws IOException, OAuth2Exception, InterruptedException, ExecutionException { + OAuth2AccessToken accessToken = service.getAccessToken(code); - - final String userEndpoint = getUserEndpoint(accessToken); + + // We need to check if scope is null first: GitHub is used without scope, so the responses scope is null. + // Checking scopes via Stream to be independent from order. + if ( ( accessToken.getScope() != null && ! getScope().stream().allMatch(accessToken.getScope()::contains) ) || + ( accessToken.getScope() == null && ! getSpacedScope().isEmpty() ) ) { + // We did not get the permissions on the scope(s) we need. Abort and inform the user. + throw new OAuth2Exception(200, BundleUtil.getStringFromBundle("auth.providers.insufficientScope", Arrays.asList(this.getTitle())), ""); + } - final OAuthRequest request = new OAuthRequest(Verb.GET, userEndpoint, service); - request.addHeader("Authorization", "Bearer " + accessToken.getAccessToken()); + OAuthRequest request = new OAuthRequest(Verb.GET, getUserEndpoint(accessToken)); request.setCharset("UTF-8"); - - final Response response = request.send(); + service.signRequest(accessToken, request); + + Response response = service.execute(request); int responseCode = response.getCode(); - final String body = response.getBody(); - logger.log(Level.FINE, "In getUserRecord. Body: {0}", body); - - if ( responseCode == 200 ) { - final ParsedUserResponse parsed = parseUserResponse(body); - return new OAuth2UserRecord(getId(), parsed.userIdInProvider, - parsed.username, - OAuth2TokenData.from(accessToken), - parsed.displayInfo, - parsed.emails); + String body = response.getBody(); + logger.log(Level.FINE, "In requestUserRecord. Body: {0}", body); + if ( responseCode == 200 && body != null ) { + return getUserRecord(body, accessToken, service); } else { - throw new OAuth2Exception(responseCode, body, "Error getting the user info record."); + throw new OAuth2Exception(responseCode, body, BundleUtil.getStringFromBundle("auth.providers.exception.userinfo", Arrays.asList(this.getTitle()))); } } + + /** + * Get the user record from the response body. + * Might be overriden by subclasses to add information from the access token response not included + * within the request response body. + * @param accessToken Access token used to create the request + * @param responseBody The response body = message from provider + * @param service Not used in base class, but may be used in overrides to lookup more data + * @return A complete record to be forwarded to user handling logic + * @throws OAuth2Exception When some lookup fails in overrides + */ + protected OAuth2UserRecord getUserRecord(@NotNull String responseBody, @NotNull OAuth2AccessToken accessToken, @NotNull OAuth20Service service) + throws OAuth2Exception { + + final ParsedUserResponse parsed = parseUserResponse(responseBody); + return new OAuth2UserRecord(getId(), parsed.userIdInProvider, + parsed.username, + OAuth2TokenData.from(accessToken), + parsed.displayInfo, + parsed.emails); + } @Override public boolean isUserInfoUpdateAllowed() { @@ -195,6 +233,10 @@ public void setSubTitle(String subtitle) { public String getSubTitle() { return subTitle; } + + public List getScope() { return scope; } + + public String getSpacedScope() { return String.join(" ", getScope()); } @Override public int hashCode() { diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java index 44bafa980fe..5a43c255fcf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java @@ -1,5 +1,7 @@ package edu.harvard.iq.dataverse.authorization.providers.oauth2; +import com.github.scribejava.core.oauth.AuthorizationUrlBuilder; +import com.github.scribejava.core.oauth.OAuth20Service; import edu.harvard.iq.dataverse.DataverseSession; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; @@ -8,9 +10,11 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.Serializable; +import java.security.SecureRandom; import java.util.Comparator; import java.util.List; import java.util.Optional; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; import static java.util.stream.Collectors.toList; @@ -21,6 +25,8 @@ import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.validation.constraints.NotNull; + import static edu.harvard.iq.dataverse.util.StringUtil.toOption; import edu.harvard.iq.dataverse.util.SystemConfig; @@ -57,19 +63,77 @@ public class OAuth2LoginBackingBean implements Serializable { @Inject OAuth2FirstLoginPage newAccountPage; + /** + * Generate the OAuth2 Provider URL to be used in the login page link for the provider. + * @param idpId Unique ID for the provider (used to lookup in authn service bean) + * @param redirectPage page part of URL where we should be redirected after login (e.g. "dataverse.xhtml") + * @return A generated link for the OAuth2 provider login + */ public String linkFor(String idpId, String redirectPage) { AbstractOAuth2AuthenticationProvider idp = authenticationSvc.getOAuth2Provider(idpId); - return idp.getService(createState(idp, toOption(redirectPage) ), getCallbackUrl()).getAuthorizationUrl(); - } - - public String getCallbackUrl() { - return systemConfig.getOAuth2CallbackUrl(); + OAuth20Service svc = idp.getService(systemConfig.getOAuth2CallbackUrl()); + String state = createState(idp, toOption(redirectPage)); + + AuthorizationUrlBuilder aub = svc.createAuthorizationUrlBuilder() + .state(state); + + // Do not include scope if empty string (necessary for GitHub) + if (!idp.getSpacedScope().isEmpty()) { aub.scope(idp.getSpacedScope()); } + + return aub.build(); } - + + /** + * View action for callback.xhtml, the browser redirect target for the OAuth2 provider. + * @throws IOException + */ public void exchangeCodeForToken() throws IOException { HttpServletRequest req = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); + + try { + Optional oIdp = parseStateFromRequest(req); + Optional code = parseCodeFromRequest(req); - final String code = req.getParameter("code"); + if (oIdp.isPresent() && code.isPresent()) { + AbstractOAuth2AuthenticationProvider idp = oIdp.get(); + + OAuth20Service svc = idp.getService(systemConfig.getOAuth2CallbackUrl()); + oauthUser = idp.getUserRecord(code.get(), svc); + + UserRecordIdentifier idtf = oauthUser.getUserRecordIdentifier(); + AuthenticatedUser dvUser = authenticationSvc.lookupUser(idtf); + + if (dvUser == null) { + // need to create the user + newAccountPage.setNewUser(oauthUser); + FacesContext.getCurrentInstance().getExternalContext().redirect("/oauth2/firstLogin.xhtml"); + + } else { + // login the user and redirect to HOME of intended page (if any). + session.setUser(dvUser); + session.configureSessionTimeout(); + final OAuth2TokenData tokenData = oauthUser.getTokenData(); + tokenData.setUser(dvUser); + tokenData.setOauthProviderId(idp.getId()); + oauth2Tokens.store(tokenData); + String destination = redirectPage.orElse("/"); + HttpServletResponse response = (HttpServletResponse) FacesContext.getCurrentInstance().getExternalContext().getResponse(); + String prettyUrl = response.encodeRedirectURL(destination); + FacesContext.getCurrentInstance().getExternalContext().redirect(prettyUrl); + } + } + } catch (OAuth2Exception ex) { + error = ex; + logger.log(Level.INFO, "OAuth2Exception caught. HTTP return code: {0}. Message: {1}. Message body: {2}", new Object[]{error.getHttpReturnCode(), error.getLocalizedMessage(), error.getMessageBody()}); + Logger.getLogger(OAuth2LoginBackingBean.class.getName()).log(Level.SEVERE, null, ex); + } catch (InterruptedException | ExecutionException ex) { + error = new OAuth2Exception(-1, "Please see server logs for more details", "Could not login due to threading exceptions."); + logger.log(Level.WARNING, "Threading exception caught. Message: {0}", ex.getLocalizedMessage()); + } + } + + private Optional parseCodeFromRequest(@NotNull HttpServletRequest req) { + String code = req.getParameter("code"); if (code == null || code.trim().isEmpty()) { try (BufferedReader rdr = req.getReader()) { StringBuilder sb = new StringBuilder(); @@ -79,60 +143,36 @@ public void exchangeCodeForToken() throws IOException { } error = new OAuth2Exception(-1, sb.toString(), "Remote system did not return an authorization code."); logger.log(Level.INFO, "OAuth2Exception getting code parameter. HTTP return code: {0}. Message: {1} Message body: {2}", new Object[]{error.getHttpReturnCode(), error.getLocalizedMessage(), error.getMessageBody()}); - return; + return Optional.empty(); + } catch (IOException e) { + error = new OAuth2Exception(-1, "", "Could not parse OAuth2 code due to IO error."); + logger.log(Level.WARNING, "IOException getting code parameter.", e.getLocalizedMessage()); + return Optional.empty(); } } - - final String state = req.getParameter("state"); - - try { - AbstractOAuth2AuthenticationProvider idp = parseState(state); - if (idp == null) { - throw new OAuth2Exception(-1, "", "Invalid 'state' parameter."); - } - oauthUser = idp.getUserRecord(code, state, getCallbackUrl()); - UserRecordIdentifier idtf = oauthUser.getUserRecordIdentifier(); - AuthenticatedUser dvUser = authenticationSvc.lookupUser(idtf); - - if (dvUser == null) { - // need to create the user - newAccountPage.setNewUser(oauthUser); - FacesContext.getCurrentInstance().getExternalContext().redirect("/oauth2/firstLogin.xhtml"); - - } else { - // login the user and redirect to HOME of intended page (if any). - session.setUser(dvUser); - session.configureSessionTimeout(); - - final OAuth2TokenData tokenData = oauthUser.getTokenData(); - tokenData.setUser(dvUser); - tokenData.setOauthProviderId(idp.getId()); - oauth2Tokens.store(tokenData); - String destination = redirectPage.orElse("/"); - HttpServletResponse response = (HttpServletResponse) FacesContext.getCurrentInstance().getExternalContext().getResponse(); - String prettyUrl = response.encodeRedirectURL(destination); - FacesContext.getCurrentInstance().getExternalContext().redirect(prettyUrl); - } - - } catch (OAuth2Exception ex) { - error = ex; - logger.log(Level.INFO, "OAuth2Exception caught. HTTP return code: {0}. Message: {1}. Message body: {2}", new Object[]{error.getHttpReturnCode(), error.getLocalizedMessage(), error.getMessageBody()}); - Logger.getLogger(OAuth2LoginBackingBean.class.getName()).log(Level.SEVERE, null, ex); - } - + return Optional.of(code); } - private AbstractOAuth2AuthenticationProvider parseState(String state) { + private Optional parseStateFromRequest(@NotNull HttpServletRequest req) { + String state = req.getParameter("state"); + + if (state == null) { + logger.log(Level.INFO, "No state present in request"); + return Optional.empty(); + } + String[] topFields = state.split("~", 2); if (topFields.length != 2) { logger.log(Level.INFO, "Wrong number of fields in state string", state); - return null; + return Optional.empty(); } AbstractOAuth2AuthenticationProvider idp = authenticationSvc.getOAuth2Provider(topFields[0]); if (idp == null) { logger.log(Level.INFO, "Can''t find IDP ''{0}''", topFields[0]); - return null; + return Optional.empty(); } + + // Verify the response by decrypting values and check for state valid timeout String raw = StringUtil.decrypt(topFields[1], idp.clientSecret); String[] stateFields = raw.split("~", -1); if (idp.getId().equals(stateFields[0])) { @@ -142,14 +182,14 @@ private AbstractOAuth2AuthenticationProvider parseState(String state) { if ( stateFields.length > 3) { redirectPage = Optional.ofNullable(stateFields[3]); } - return idp; + return Optional.of(idp); } else { logger.info("State timeout"); - return null; + return Optional.empty(); } } else { logger.log(Level.INFO, "Invalid id field: ''{0}''", stateFields[0]); - return null; + return Optional.empty(); } } @@ -157,8 +197,10 @@ private String createState(AbstractOAuth2AuthenticationProvider idp, Optional "~"+page).orElse(""); String encrypted = StringUtil.encrypt(base, idp.clientSecret); diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java index db29bae92bd..a5ee5ddf537 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java @@ -58,9 +58,6 @@ public class OAuth2TokenData implements Serializable { @Column(length = 64) private String refreshToken; - @Column(length = 64) - private String scope; - @Column(length = 32) private String tokenType; @@ -78,7 +75,6 @@ public static OAuth2TokenData from( OAuth2AccessToken accessTokenResponse ) { OAuth2TokenData retVal = new OAuth2TokenData(); retVal.setAccessToken(accessTokenResponse.getAccessToken()); retVal.setRefreshToken( accessTokenResponse.getRefreshToken() ); - retVal.setScope( accessTokenResponse.getScope() ); retVal.setTokenType( accessTokenResponse.getTokenType() ); if ( accessTokenResponse.getExpiresIn() != null ) { retVal.setExpiryDate( new Timestamp( System.currentTimeMillis() + accessTokenResponse.getExpiresIn())); @@ -136,14 +132,6 @@ public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; } - public String getScope() { - return scope; - } - - public void setScope(String scope) { - this.scope = scope; - } - public String getTokenType() { return tokenType; } 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 895fe80738e..62f3cc382e2 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 @@ -1,7 +1,7 @@ package edu.harvard.iq.dataverse.authorization.providers.oauth2.impl; import com.github.scribejava.apis.GitHubApi; -import com.github.scribejava.core.builder.api.BaseApi; +import com.github.scribejava.core.builder.api.DefaultApi20; import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; import edu.harvard.iq.dataverse.authorization.providers.oauth2.AbstractOAuth2AuthenticationProvider; import edu.harvard.iq.dataverse.authorization.providers.shib.ShibUserNameFields; @@ -28,7 +28,7 @@ public GitHubOAuth2AP(String aClientId, String aClientSecret) { } @Override - public BaseApi getApiInstance() { + public DefaultApi20 getApiInstance() { return GitHubApi.instance(); } 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 62926a03463..1fa5470d551 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 @@ -1,11 +1,12 @@ package edu.harvard.iq.dataverse.authorization.providers.oauth2.impl; import com.github.scribejava.apis.GoogleApi20; -import com.github.scribejava.core.builder.api.BaseApi; +import com.github.scribejava.core.builder.api.DefaultApi20; import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; import edu.harvard.iq.dataverse.authorization.providers.oauth2.AbstractOAuth2AuthenticationProvider; import edu.harvard.iq.dataverse.util.BundleUtil; import java.io.StringReader; +import java.util.Arrays; import java.util.UUID; import javax.json.Json; import javax.json.JsonObject; @@ -22,12 +23,12 @@ public GoogleOAuth2AP(String aClientId, String aClientSecret) { title = BundleUtil.getStringFromBundle("auth.providers.title.google"); clientId = aClientId; clientSecret = aClientSecret; - scope = "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email"; + scope = Arrays.asList("https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email"); baseUserEndpoint = "https://www.googleapis.com/oauth2/v2/userinfo"; } @Override - public BaseApi getApiInstance() { + public DefaultApi20 getApiInstance() { return GoogleApi20.instance(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/impl/OrcidOAuth2AP.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/impl/OrcidOAuth2AP.java index dbc4b0ac4e6..be22e9bc332 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/impl/OrcidOAuth2AP.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/impl/OrcidOAuth2AP.java @@ -1,6 +1,6 @@ package edu.harvard.iq.dataverse.authorization.providers.oauth2.impl; -import com.github.scribejava.core.builder.api.BaseApi; +import com.github.scribejava.core.builder.api.DefaultApi20; import com.github.scribejava.core.model.OAuth2AccessToken; import com.github.scribejava.core.model.OAuthRequest; import com.github.scribejava.core.model.Response; @@ -15,11 +15,8 @@ import edu.harvard.iq.dataverse.util.BundleUtil; import java.io.IOException; import java.io.StringReader; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; +import java.util.*; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -29,6 +26,7 @@ import javax.json.Json; import javax.json.JsonObject; import javax.json.JsonReader; +import javax.validation.constraints.NotNull; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -57,7 +55,7 @@ public class OrcidOAuth2AP extends AbstractOAuth2AuthenticationProvider { public static final String PROVIDER_ID_SANDBOX = "orcid-sandbox"; public OrcidOAuth2AP(String clientId, String clientSecret, String userEndpoint) { - scope = "/read-limited"; + scope = Arrays.asList("/read-limited"); this.clientId = clientId; this.clientSecret = clientSecret; this.baseUserEndpoint = userEndpoint; @@ -73,47 +71,36 @@ public String getUserEndpoint( OAuth2AccessToken token ) { } @Override - public BaseApi getApiInstance() { + public DefaultApi20 getApiInstance() { return OrcidApi.instance( ! baseUserEndpoint.contains("sandbox") ); } @Override - public OAuth2UserRecord getUserRecord(String code, String state, String redirectUrl) throws IOException, OAuth2Exception { - OAuth20Service service = getService(state, redirectUrl); - OAuth2AccessToken accessToken = service.getAccessToken(code); + final protected OAuth2UserRecord getUserRecord(@NotNull String responseBody, @NotNull OAuth2AccessToken accessToken, @NotNull OAuth20Service service) + throws OAuth2Exception { - if ( ! accessToken.getScope().contains(scope) ) { - // We did not get the permissions on the scope we need. Abort and inform the user. - throw new OAuth2Exception(200, BundleUtil.getStringFromBundle("auth.providers.orcid.insufficientScope"), ""); + // parse the main response + final ParsedUserResponse parsed = parseUserResponse(responseBody); + + // mixin org data, but optional + try { + Optional orgData = getOrganizationalData(accessToken, service); + if (orgData.isPresent()) { + parsed.displayInfo.setAffiliation(orgData.get().getAffiliation()); + parsed.displayInfo.setPosition(orgData.get().getPosition()); + } + } catch (IOException ex) { + logger.log(Level.WARNING, "Could not get affiliation data from ORCiD due to an IO problem: {0}", ex.getLocalizedMessage()); } + // mixin ORCiD not present in main response String orcidNumber = extractOrcidNumber(accessToken.getRawResponse()); - - final String userEndpoint = getUserEndpoint(accessToken); - - final OAuthRequest request = new OAuthRequest(Verb.GET, userEndpoint, service); - request.addHeader("Authorization", "Bearer " + accessToken.getAccessToken()); - request.setCharset("UTF-8"); - - final Response response = request.send(); - int responseCode = response.getCode(); - final String body = response.getBody(); - logger.log(Level.FINE, "In getUserRecord. Body: {0}", body); - - if ( responseCode == 200 ) { - final ParsedUserResponse parsed = parseUserResponse(body); - AuthenticatedUserDisplayInfo orgData = getOrganizationalData(userEndpoint, accessToken.getAccessToken(), service); - parsed.displayInfo.setAffiliation(orgData.getAffiliation()); - parsed.displayInfo.setPosition(orgData.getPosition()); - - return new OAuth2UserRecord(getId(), orcidNumber, - parsed.username, - OAuth2TokenData.from(accessToken), - parsed.displayInfo, - parsed.emails); - } else { - throw new OAuth2Exception(responseCode, body, "Error getting the user info record."); - } + + return new OAuth2UserRecord(getId(), orcidNumber, + parsed.username, + OAuth2TokenData.from(accessToken), + parsed.displayInfo, + parsed.emails); } @Override @@ -280,23 +267,29 @@ protected String extractOrcidNumber( String rawResponse ) throws OAuth2Exception } } - protected AuthenticatedUserDisplayInfo getOrganizationalData(String userEndpoint, String accessToken, OAuth20Service service) throws IOException { - final OAuthRequest request = new OAuthRequest(Verb.GET, userEndpoint.replace("/person", "/employments"), service); - request.addHeader("Authorization", "Bearer " + accessToken); - request.setCharset("UTF-8"); + protected Optional getOrganizationalData(OAuth2AccessToken accessToken, OAuth20Service service) throws IOException { - final Response response = request.send(); - int responseCode = response.getCode(); - final String responseBody = response.getBody(); + OAuthRequest request = new OAuthRequest(Verb.GET, getUserEndpoint(accessToken).replace("/person", "/employments")); + request.setCharset("UTF-8"); + service.signRequest(accessToken, request); - if ( responseCode != 200 ) { - // This is bad, but not bad enough to stop a signup/in process. - logger.log(Level.WARNING, "Cannot get affiliation data from ORCiD. Response code: {0} body:\n{1}\n/body", - new Object[]{responseCode, responseBody}); - return null; - - } else { - return parseActivitiesResponse(responseBody); + try { + Response response = service.execute(request); + int responseCode = response.getCode(); + String responseBody = response.getBody(); + + if (responseCode != 200 && responseBody != null) { + // This is bad, but not bad enough to stop a signup/in process. + logger.log(Level.WARNING, "Cannot get affiliation data from ORCiD. Response code: {0} body:\n{1}\n/body", + new Object[]{responseCode, responseBody}); + return Optional.empty(); + + } else { + return Optional.of(parseActivitiesResponse(responseBody)); + } + } catch (InterruptedException | ExecutionException ex) { + logger.log(Level.WARNING, "Could not get affiliation data from ORCiD due to threading problems."); + return Optional.empty(); } } diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 83c7d377a2a..d12d3321f5f 100755 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -312,7 +312,8 @@ auth.providers.persistentUserIdName.orcid=ORCID iD auth.providers.persistentUserIdName.github=ID auth.providers.persistentUserIdTooltip.orcid=ORCID provides a persistent digital identifier that distinguishes you from other researchers. auth.providers.persistentUserIdTooltip.github=GitHub assigns a unique number to every user. -auth.providers.orcid.insufficientScope=Dataverse was not granted the permission to read user data from ORCID. +auth.providers.insufficientScope=Dataverse was not granted the permission to read user data from {0}. +auth.providers.exception.userinfo=Error getting the user info record from {0}. auth.providers.orcid.helpmessage1=ORCID is an open, non-profit, community-based effort to provide a registry of unique researcher identifiers and a transparent method of linking research activities and outputs to these identifiers. ORCID is unique in its ability to reach across disciplines, research sectors, and national boundaries and its cooperation with other identifier systems. Find out more at orcid.org/about. auth.providers.orcid.helpmessage2=This repository uses your ORCID for authentication (so you don't need another username/password combination). Having your ORCID associated with your datasets also makes it easier for people to find the datasets you have published. diff --git a/src/main/resources/db/migration/V4.18.0.1__5991-update-scribejava.sql b/src/main/resources/db/migration/V4.18.0.1__5991-update-scribejava.sql new file mode 100644 index 00000000000..6762e1fc076 --- /dev/null +++ b/src/main/resources/db/migration/V4.18.0.1__5991-update-scribejava.sql @@ -0,0 +1 @@ +ALTER TABLE OAuth2TokenData DROP COLUMN IF EXISTS scope; \ No newline at end of file