From 13465b7b1b8762b90e8c8422ca47dbb843160836 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Thu, 4 Jul 2019 12:31:50 +0200 Subject: [PATCH 01/16] #5991 - Update from ScribeJava v3.1.0 to v6.6.3 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 78b2c0ca7cb..0431ee27afc 100644 --- a/pom.xml +++ b/pom.xml @@ -501,7 +501,7 @@ com.github.scribejava scribejava-apis - 3.1.0 + 6.6.3 From 9aa7fe85fe0cbb6630072d61c874bb6dbd0b7acc Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 5 Jul 2019 10:12:35 +0200 Subject: [PATCH 02/16] #5991. Change BaseAPI to DefaultApi20, as upstream removed the old one. --- .../authorization/providers/oauth2/impl/GitHubOAuth2AP.java | 4 ++-- .../authorization/providers/oauth2/impl/GoogleOAuth2AP.java | 4 ++-- .../authorization/providers/oauth2/impl/OrcidOAuth2AP.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) 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..ed8882e2075 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,7 +1,7 @@ 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; @@ -27,7 +27,7 @@ public GoogleOAuth2AP(String aClientId, String aClientSecret) { } @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..b6ebd3dfe48 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; @@ -73,7 +73,7 @@ public String getUserEndpoint( OAuth2AccessToken token ) { } @Override - public BaseApi getApiInstance() { + public DefaultApi20 getApiInstance() { return OrcidApi.instance( ! baseUserEndpoint.contains("sandbox") ); } From 1a4e449980e986f0ca878cc9eb958f3348c39767 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 5 Jul 2019 12:48:34 +0200 Subject: [PATCH 03/16] #5991. Refactor OAuth login bean and provider superclass to be compatible with recent ScribeJava library. Refactored code structure a bit, too. --- .../AbstractOAuth2AuthenticationProvider.java | 58 +++++--- .../oauth2/OAuth2LoginBackingBean.java | 138 +++++++++++------- 2 files changed, 125 insertions(+), 71 deletions(-) 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..40a2eb09572 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,15 @@ import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; import edu.harvard.iq.dataverse.authorization.AuthenticationProvider; import edu.harvard.iq.dataverse.authorization.AuthenticationProviderDisplayInfo; + +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.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; @@ -91,35 +94,48 @@ public String toString() { protected String redirectUrl; protected String scope; - 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()) + .defaultScope(getScope()) + .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); + String userEndpoint = getUserEndpoint(accessToken); - final OAuthRequest request = new OAuthRequest(Verb.GET, userEndpoint, service); - request.addHeader("Authorization", "Bearer " + accessToken.getAccessToken()); + OAuthRequest request = new OAuthRequest(Verb.GET, userEndpoint); request.setCharset("UTF-8"); + service.signRequest(accessToken, request); - final Response response = request.send(); + Response response = service.execute(request); int responseCode = response.getCode(); - final String body = response.getBody(); + String body = response.getBody(); logger.log(Level.FINE, "In getUserRecord. Body: {0}", body); if ( responseCode == 200 ) { @@ -195,6 +211,8 @@ public void setSubTitle(String subtitle) { public String getSubTitle() { return subTitle; } + + public String getScope() { return scope; } @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 6fdc33b48b3..57933b5a26c 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,6 @@ package edu.harvard.iq.dataverse.authorization.providers.oauth2; +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; @@ -11,6 +12,7 @@ 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 +23,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 +61,73 @@ 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)); + + return svc.createAuthorizationUrlBuilder() + .state(state) + .scope(idp.getScope()) + .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(); - - final String code = req.getParameter("code"); + + try { + Optional oIdp = parseStateFromRequest(req); + Optional code = parseCodeFromRequest(req); + + 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); + 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,58 +137,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; - } - } - - 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); - 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); + 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(); } - - } 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])) { @@ -140,14 +176,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(); } } From 26af35e05100087d7517027c39c2b15d23e30335 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 5 Jul 2019 13:32:25 +0200 Subject: [PATCH 04/16] #5991. Refactor ORCID provider to use current ScribeJava lib. Refactor base class to avoid code duplication when generating the user record. --- .../AbstractOAuth2AuthenticationProvider.java | 44 ++++++--- .../providers/oauth2/impl/OrcidOAuth2AP.java | 97 +++++++++---------- 2 files changed, 77 insertions(+), 64 deletions(-) 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 40a2eb09572..b8bdbfa324f 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 @@ -10,6 +10,7 @@ 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; @@ -127,28 +128,47 @@ public OAuth2UserRecord getUserRecord(String code, @NotNull OAuth20Service servi throws IOException, OAuth2Exception, InterruptedException, ExecutionException { OAuth2AccessToken accessToken = service.getAccessToken(code); - String userEndpoint = getUserEndpoint(accessToken); + + 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"), ""); + } - OAuthRequest request = new OAuthRequest(Verb.GET, userEndpoint); + OAuthRequest request = new OAuthRequest(Verb.GET, getUserEndpoint(accessToken)); request.setCharset("UTF-8"); service.signRequest(accessToken, request); - + Response response = service.execute(request); int responseCode = response.getCode(); 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); + 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."); } } + + /** + * 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() { 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 b6ebd3dfe48..8d530e3b7e0 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 @@ -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; @@ -78,42 +76,31 @@ public DefaultApi20 getApiInstance() { } @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(); } } From 69b5f07ff99dddc8a4df74fcef834696a49076e1 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 13 Sep 2019 10:57:31 +0200 Subject: [PATCH 05/16] Update to ScribeJava 6.8.1 from 6.6.3. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4b86bcba358..95c980515bf 100644 --- a/pom.xml +++ b/pom.xml @@ -500,7 +500,7 @@ com.github.scribejava scribejava-apis - 6.6.3 + 6.8.1 From 5a79443c21a2c5d4ee5793cf08ba2219c26c4785 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 13 Sep 2019 11:13:55 +0200 Subject: [PATCH 06/16] Make OAuth2 state creation use secure RNG. --- .../providers/oauth2/OAuth2LoginBackingBean.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 b52ef07331d..aec6726555f 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 @@ -9,6 +9,7 @@ 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; @@ -192,8 +193,10 @@ private String createState(AbstractOAuth2AuthenticationProvider idp, Optional "~"+page).orElse(""); String encrypted = StringUtil.encrypt(base, idp.clientSecret); From 55a27126904d0cf3fd7e0386cd9262d864aa155e Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 13 Sep 2019 14:09:07 +0200 Subject: [PATCH 07/16] Make OAuth2LoginBackingBean testable with injected FacesContext --- .../providers/oauth2/OAuth2LoginBackingBean.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 aec6726555f..52c703b9fb0 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 @@ -18,6 +18,7 @@ import java.util.logging.Logger; import static java.util.stream.Collectors.toList; import javax.ejb.EJB; +import javax.faces.bean.ManagedProperty; import javax.faces.context.FacesContext; import javax.inject.Named; import javax.faces.view.ViewScoped; @@ -61,6 +62,9 @@ public class OAuth2LoginBackingBean implements Serializable { @Inject OAuth2FirstLoginPage newAccountPage; + + @ManagedProperty("#{facesContext}") + FacesContext facesCtx; /** * Generate the OAuth2 Provider URL to be used in the login page link for the provider. @@ -84,7 +88,7 @@ public String linkFor(String idpId, String redirectPage) { * @throws IOException */ public void exchangeCodeForToken() throws IOException { - HttpServletRequest req = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); + HttpServletRequest req = (HttpServletRequest) facesCtx.getExternalContext().getRequest(); try { Optional oIdp = parseStateFromRequest(req); From 461214124ae56e400f6410f796ebb5435f7422e6 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 20 Sep 2019 13:10:03 +0200 Subject: [PATCH 08/16] Revert "Make OAuth2LoginBackingBean testable with injected FacesContext". This is not usable on Glassfish 4.1. This reverts commit 55a27126904d0cf3fd7e0386cd9262d864aa155e. --- .../providers/oauth2/OAuth2LoginBackingBean.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 52c703b9fb0..aec6726555f 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 @@ -18,7 +18,6 @@ import java.util.logging.Logger; import static java.util.stream.Collectors.toList; import javax.ejb.EJB; -import javax.faces.bean.ManagedProperty; import javax.faces.context.FacesContext; import javax.inject.Named; import javax.faces.view.ViewScoped; @@ -62,9 +61,6 @@ public class OAuth2LoginBackingBean implements Serializable { @Inject OAuth2FirstLoginPage newAccountPage; - - @ManagedProperty("#{facesContext}") - FacesContext facesCtx; /** * Generate the OAuth2 Provider URL to be used in the login page link for the provider. @@ -88,7 +84,7 @@ public String linkFor(String idpId, String redirectPage) { * @throws IOException */ public void exchangeCodeForToken() throws IOException { - HttpServletRequest req = (HttpServletRequest) facesCtx.getExternalContext().getRequest(); + HttpServletRequest req = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); try { Optional oIdp = parseStateFromRequest(req); From a51497f378066b3549d6a4b443e15db6d420c07b Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Mon, 23 Sep 2019 13:09:09 +0200 Subject: [PATCH 09/16] Make scope checking after authorization multi-scope aware. The list of authorized scopes doesn't have to be in the same order as we send it. So better check the single scope names on their own. Relates to #5991. --- .../oauth2/AbstractOAuth2AuthenticationProvider.java | 12 +++++++----- .../providers/oauth2/OAuth2LoginBackingBean.java | 2 +- .../providers/oauth2/impl/GoogleOAuth2AP.java | 3 ++- .../providers/oauth2/impl/OrcidOAuth2AP.java | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) 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 b8bdbfa324f..5d02a5c7a17 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 @@ -93,7 +93,7 @@ public String toString() { protected String clientSecret; protected String baseUserEndpoint; protected String redirectUrl; - protected String scope; + protected List scope; public abstract DefaultApi20 getApiInstance(); @@ -108,7 +108,7 @@ public String toString() { public OAuth20Service getService(String callbackUrl) { return new ServiceBuilder(getClientId()) .apiSecret(getClientSecret()) - .defaultScope(getScope()) + .defaultScope(getSpacedScope()) .callback(callbackUrl) .build(getApiInstance()); } @@ -129,8 +129,8 @@ public OAuth2UserRecord getUserRecord(String code, @NotNull OAuth20Service servi OAuth2AccessToken accessToken = service.getAccessToken(code); - if ( ! accessToken.getScope().contains(scope) ) { - // We did not get the permissions on the scope we need. Abort and inform the user. + if ( !getScope().stream().allMatch(accessToken.getScope()::contains) ) { + // 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.orcid.insufficientScope"), ""); } @@ -232,7 +232,9 @@ public String getSubTitle() { return subTitle; } - public String getScope() { return scope; } + public List getScope() { return scope; } + + public String getSpacedScope() { return String.join(" ", scope); } @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 aec6726555f..dcbe374be2d 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 @@ -75,7 +75,7 @@ public String linkFor(String idpId, String redirectPage) { return svc.createAuthorizationUrlBuilder() .state(state) - .scope(idp.getScope()) + .scope(idp.getSpacedScope()) .build(); } 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 ed8882e2075..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 @@ -6,6 +6,7 @@ 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,7 +23,7 @@ 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"; } 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 8d530e3b7e0..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 @@ -55,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; From 81b367b790a20bbade9b0493451a7bd39921da95 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Mon, 23 Sep 2019 16:40:33 +0200 Subject: [PATCH 10/16] Remove scope attribute from OAuth2TokenData. Since the introduction of OAuth2 the scope attribute of all saved tokens has been "null", as there seemed to be a bug with ScribeJava. Upgrading from v3.3.6 to v6.8.1 resulted in exceptions due to the scope being saved now, but violating the 64 char limit. As the persisted data has not been in use ever since (the scope is always retrieved from the IdP implementation), the attribute has been removed to save database space and avoid the exception. An appropriate SQL migration script for Flyway has been added. Relates to #5991. --- .../providers/oauth2/OAuth2TokenData.java | 12 ------------ .../migration/V4.16.0.3__5991-update-scribejava.sql | 1 + 2 files changed, 1 insertion(+), 12 deletions(-) create mode 100644 src/main/resources/db/migration/V4.16.0.3__5991-update-scribejava.sql 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/resources/db/migration/V4.16.0.3__5991-update-scribejava.sql b/src/main/resources/db/migration/V4.16.0.3__5991-update-scribejava.sql new file mode 100644 index 00000000000..6762e1fc076 --- /dev/null +++ b/src/main/resources/db/migration/V4.16.0.3__5991-update-scribejava.sql @@ -0,0 +1 @@ +ALTER TABLE OAuth2TokenData DROP COLUMN IF EXISTS scope; \ No newline at end of file From 391696672bddcaf41b78032cd8e96118b07cdac4 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 24 Sep 2019 15:34:11 +0200 Subject: [PATCH 11/16] Refactor scope usage to make it optional. The GitHub auth provider had been implemented with no scope. Thus only public information is used, the user needs to provide his or her mail address on first login page. Relates to #5991. --- .../AbstractOAuth2AuthenticationProvider.java | 25 ++++++++++--------- .../oauth2/OAuth2LoginBackingBean.java | 12 ++++++--- 2 files changed, 21 insertions(+), 16 deletions(-) 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 5d02a5c7a17..4b83e5a9427 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 @@ -14,11 +14,7 @@ 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; @@ -93,7 +89,11 @@ public String toString() { protected String clientSecret; protected String baseUserEndpoint; protected String redirectUrl; - protected List 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 DefaultApi20 getApiInstance(); @@ -107,10 +107,9 @@ public String toString() { */ public OAuth20Service getService(String callbackUrl) { return new ServiceBuilder(getClientId()) - .apiSecret(getClientSecret()) - .defaultScope(getSpacedScope()) - .callback(callbackUrl) - .build(getApiInstance()); + .apiSecret(getClientSecret()) + .callback(callbackUrl) + .build(getApiInstance()); } /** @@ -129,7 +128,9 @@ public OAuth2UserRecord getUserRecord(String code, @NotNull OAuth20Service servi OAuth2AccessToken accessToken = service.getAccessToken(code); - if ( !getScope().stream().allMatch(accessToken.getScope()::contains) ) { + // 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) ) { // 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.orcid.insufficientScope"), ""); } @@ -234,7 +235,7 @@ public String getSubTitle() { public List getScope() { return scope; } - public String getSpacedScope() { return String.join(" ", 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 dcbe374be2d..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,6 @@ 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; @@ -73,10 +74,13 @@ public String linkFor(String idpId, String redirectPage) { OAuth20Service svc = idp.getService(systemConfig.getOAuth2CallbackUrl()); String state = createState(idp, toOption(redirectPage)); - return svc.createAuthorizationUrlBuilder() - .state(state) - .scope(idp.getSpacedScope()) - .build(); + 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(); } /** From 2f82f35f99119f7f70f043427c7d450236fe59c7 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 24 Sep 2019 22:46:57 +0200 Subject: [PATCH 12/16] Update Mockito to 2.28.2, latest release before 3.x --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 95c980515bf..009c82fad0b 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 From c729a27bac60e90db89a2fe0f54cc610e3eb8d3f Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 18 Oct 2019 00:27:05 +0200 Subject: [PATCH 16/16] Fix i18n for OAuth2 exceptions in AbstractOAuth2AuthenticationProvider --- .../oauth2/AbstractOAuth2AuthenticationProvider.java | 4 ++-- src/main/java/propertyFiles/Bundle.properties | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) 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 861da47e8d4..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 @@ -133,7 +133,7 @@ public OAuth2UserRecord getUserRecord(String code, @NotNull OAuth20Service servi 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.orcid.insufficientScope"), ""); + throw new OAuth2Exception(200, BundleUtil.getStringFromBundle("auth.providers.insufficientScope", Arrays.asList(this.getTitle())), ""); } OAuthRequest request = new OAuthRequest(Verb.GET, getUserEndpoint(accessToken)); @@ -147,7 +147,7 @@ public OAuth2UserRecord getUserRecord(String code, @NotNull OAuth20Service servi 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()))); } } 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.