From e146e219c640ec5bbf502b05668c13039529c629 Mon Sep 17 00:00:00 2001 From: Michael Bar-Sinai Date: Mon, 12 Sep 2016 00:26:13 +0300 Subject: [PATCH] Added Google Authentication. More work on ORCiD --- .gitignore | 4 +- .../providers/oauth2/AbstractOAuth2Idp.java | 48 +++++++++++++-- .../oauth2/OAuth2AuthenticationProvider.java | 8 ++- .../providers/oauth2/OAuth2Page.java | 24 +++++--- .../providers/oauth2/OrcidApi.java | 34 ----------- .../identityproviders/GitHubOAuth2Idp.java | 5 +- .../identityproviders/GoogleOAuth2Idp.java | 50 ++++++++++++++++ .../oauth2/identityproviders/OrcidApi.java | 58 +++++++++++++++++++ .../OrcidPublicOAuth2Idp.java | 43 ++++++++++++++ src/main/webapp/oauth-callback.xhtml | 32 ++++++++++ 10 files changed, 255 insertions(+), 51 deletions(-) delete mode 100644 src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OrcidApi.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/identityproviders/GoogleOAuth2Idp.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/identityproviders/OrcidApi.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/identityproviders/OrcidPublicOAuth2Idp.java create mode 100644 src/main/webapp/oauth-callback.xhtml diff --git a/.gitignore b/.gitignore index 390a4a56ce0..48a576112e6 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,6 @@ scripts/api/py_api_wrapper/local-data/* doc/sphinx-guides/build faces-config.NavData src/main/java/BuildNumber.properties -/nbproject/ \ No newline at end of file +/nbproject/ +oauth-credentials.md + diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2Idp.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2Idp.java index a79a4dbc98b..0c3c12a9fa5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2Idp.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2Idp.java @@ -9,6 +9,7 @@ import com.github.scribejava.core.oauth.OAuth20Service; import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; import java.io.IOException; +import java.util.Objects; import java.util.logging.Logger; /** @@ -36,6 +37,7 @@ public ParsedUserResponse(AuthenticatedUserDisplayInfo displayInfo, String userI protected String userEndpoint; protected String redirectUrl; protected String imageUrl; + protected String scope; public AbstractOAuth2Idp(){} @@ -44,18 +46,21 @@ public AbstractOAuth2Idp(){} protected abstract ParsedUserResponse parseUserResponse( String responseBody ); public OAuth20Service getService(String state) { - return (OAuth20Service) new ServiceBuilder() + ServiceBuilder svcBuilder = new ServiceBuilder() .apiKey(getClientId()) .apiSecret(getClientSecret()) .state(state) - .callback(getRedirectUrl()) - .scope(getUserEndpoint()) - .build( getApiInstance() ); + .callback(getRedirectUrl()); + if ( scope != null ) { + svcBuilder.scope(scope); + } + return (OAuth20Service) svcBuilder.build( getApiInstance() ); } public OAuth2UserRecord getUserRecord(String code, String state) throws IOException, OAuth2Exception { OAuth20Service service = getService(state); OAuth2AccessToken accessToken = service.getAccessToken(code); + Logger.getAnonymousLogger().info("Token: " + accessToken.getAccessToken()); // TODO remove. final OAuthRequest request = new OAuthRequest(Verb.GET, getUserEndpoint(), service); service.signRequest(accessToken, request); @@ -66,7 +71,7 @@ public OAuth2UserRecord getUserRecord(String code, String state) throws IOExcept ParsedUserResponse parsed = parseUserResponse(body); return new OAuth2UserRecord(getId(), parsed.userIdInProvider, accessToken.getAccessToken(), parsed.displayInfo); } else { - throw new OAuth2Exception(responseCode, body, "Error while exchanging OAuth code for an access token."); + throw new OAuth2Exception(responseCode, body, "Error getting the user info record."); } } @@ -97,5 +102,38 @@ public String getImageUrl() { public String getRedirectUrl() { return redirectUrl; } + + @Override + public int hashCode() { + int hash = 7; + hash = 97 * hash + Objects.hashCode(this.id); + hash = 97 * hash + Objects.hashCode(this.clientId); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if ( ! (obj instanceof AbstractOAuth2Idp)) { + return false; + } + final AbstractOAuth2Idp other = (AbstractOAuth2Idp) obj; + if (!Objects.equals(this.id, other.id)) { + return false; + } + if (!Objects.equals(this.clientId, other.clientId)) { + return false; + } + if (!Objects.equals(this.clientSecret, other.clientSecret)) { + return false; + } + return true; + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2AuthenticationProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2AuthenticationProvider.java index ac91d5adb46..ff946c9f925 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2AuthenticationProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2AuthenticationProvider.java @@ -5,6 +5,8 @@ import edu.harvard.iq.dataverse.authorization.AuthenticationRequest; import edu.harvard.iq.dataverse.authorization.AuthenticationResponse; import edu.harvard.iq.dataverse.authorization.providers.oauth2.identityproviders.GitHubOAuth2Idp; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.identityproviders.GoogleOAuth2Idp; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.identityproviders.OrcidPublicOAuth2Idp; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -20,7 +22,9 @@ public class OAuth2AuthenticationProvider implements AuthenticationProvider { public OAuth2AuthenticationProvider() { // TODO change this to be from the DB. - registedProvider( new GitHubOAuth2Idp() ); + registerProvider( new OrcidPublicOAuth2Idp() ); + registerProvider( new GitHubOAuth2Idp() ); + registerProvider( new GoogleOAuth2Idp() ); } @Override @@ -39,7 +43,7 @@ public AuthenticationResponse authenticate(AuthenticationRequest request) { throw new UnsupportedOperationException("Does not apply for OAuth."); } - public final void registedProvider( AbstractOAuth2Idp p ) { + public final void registerProvider( AbstractOAuth2Idp p ) { providers.put( p.getId(), p ); } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2Page.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2Page.java index bdfc807d7b9..eb1adb698ad 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2Page.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2Page.java @@ -5,11 +5,11 @@ import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.util.StringUtil; +import java.io.BufferedReader; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.List; -import java.util.Random; import java.util.logging.Level; import java.util.logging.Logger; import javax.ejb.EJB; @@ -31,7 +31,7 @@ public class OAuth2Page implements Serializable { static int counter = 0; private static final Logger logger = Logger.getLogger(OAuth2Page.class.getName()); - private static final long STATE_TIMEOUT = 1000*60*10; + private static final long STATE_TIMEOUT = 1000*60*15; // 15 minutes in msec private int responseCode; private String responseBody; private OAuth2Exception error; @@ -56,10 +56,18 @@ public String linkFor( String idpId ) { public void exchangeCodeForToken() throws IOException { HttpServletRequest req = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); - // TODO check for parameter "error", or catch OAuthException. final String code = req.getParameter("code"); - if ( code == null ) { - return; + logger.info("Code: '" + code + "'"); // TODO remove + if ( code == null || code.trim().isEmpty() ) { + try( BufferedReader rdr = req.getReader() ) { + StringBuilder sb = new StringBuilder(); + String line; + while ( (line=rdr.readLine())!=null ) { + sb.append(line).append("\n"); + } + error = new OAuth2Exception(-1, sb.toString(), "Remote system did not return an authorization code."); + return; + } } final String state = req.getParameter("state"); @@ -96,6 +104,7 @@ public void exchangeCodeForToken() throws IOException { private AbstractOAuth2Idp getIdpFromState( String state ) { String[] topFields = state.split("~",2); if ( topFields.length != 2 ) { + logger.log(Level.INFO, "Wrong number of fields in state string", state); return null; } AbstractOAuth2Idp idp = oauthPrv.getProvider( topFields[0] ); @@ -115,7 +124,7 @@ private AbstractOAuth2Idp getIdpFromState( String state ) { return null; } } else { - logger.info("Invalid id field"); + logger.log(Level.INFO, "Invalid id field: ''{0}''", stateFields[0]); return null; } } @@ -124,7 +133,8 @@ private String createState( AbstractOAuth2Idp idp ) { if ( idp == null ) { throw new IllegalArgumentException("idp cannot be null"); } - String base = idp.getId() + "~" + System.currentTimeMillis(); + String base = idp.getId() + "~" + System.currentTimeMillis() + "~" + (int)java.lang.Math.round( java.lang.Math.random()*1000 ); + String encrypted = StringUtil.encrypt(base, idp.clientSecret); final String state = idp.getId() + "~" + encrypted; return state; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OrcidApi.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OrcidApi.java deleted file mode 100644 index 6039ec113fe..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OrcidApi.java +++ /dev/null @@ -1,34 +0,0 @@ -package edu.harvard.iq.dataverse.authorization.providers.oauth2; - -import com.github.scribejava.core.builder.api.DefaultApi20; - -/** - * Adaptor for ORCiD OAuth identity Provider. - * @author michael - */ -public class OrcidApi extends DefaultApi20 { - - /** - * The instance holder pattern allows for lazy creation of the intance. - */ - private static class InstanceHolder { - private static final OrcidApi INSTANCE = new OrcidApi(); - } - - private static OrcidApi instance() { - return InstanceHolder.INSTANCE; - } - - protected OrcidApi(){} - - @Override - public String getAccessTokenEndpoint() { - return "https://pub.orcid.org/oauth/token"; - } - - @Override - protected String getAuthorizationBaseUrl() { - return "https://orcid.org/oauth/authorize"; - } - -} diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/identityproviders/GitHubOAuth2Idp.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/identityproviders/GitHubOAuth2Idp.java index 4844f3605f7..61653b46d08 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/identityproviders/GitHubOAuth2Idp.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/identityproviders/GitHubOAuth2Idp.java @@ -19,10 +19,11 @@ public GitHubOAuth2Idp() { id = "github"; title = "GitHub"; clientId = "de1bf3127f3201d3e3a2"; // TODO load from config - clientSecret = "be71dc2176a37ae72b086dbc3223fc9da5a6d29c"; // TODO load from config + clientSecret = "WITHELD"; // TODO load from config userEndpoint = "https://api.github.com/user"; redirectUrl = "http://localhost:8080/oauth2/callback.xhtml"; // TODO load from config imageUrl = null; +// scope = "user"; } @Override @@ -39,7 +40,7 @@ protected ParsedUserResponse parseUserResponse( String responseBody ) { AuthenticatedUserDisplayInfo displayInfo = new AuthenticatedUserDisplayInfo( response.getString("name",""), - "", // Github has no concept of family name + "", // Github has no concept of a family name response.getString("email",""), response.getString("company",""), "" diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/identityproviders/GoogleOAuth2Idp.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/identityproviders/GoogleOAuth2Idp.java new file mode 100644 index 00000000000..0cbabff0bcd --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/identityproviders/GoogleOAuth2Idp.java @@ -0,0 +1,50 @@ +package edu.harvard.iq.dataverse.authorization.providers.oauth2.identityproviders; + +import com.github.scribejava.apis.GoogleApi20; +import com.github.scribejava.core.builder.api.BaseApi; +import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.AbstractOAuth2Idp; +import java.io.StringReader; +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonReader; + +/** + * + * @author michael + */ +public class GoogleOAuth2Idp extends AbstractOAuth2Idp { + + public GoogleOAuth2Idp() { + id = "google"; + title = "Google"; + clientId = "372743369730-il7ohtgdemnvd2pqto8thluqgmpvu94b.apps.googleusercontent.com"; + clientSecret = "WITHELD"; + redirectUrl = "http://localhost:8080/oauth/callback.xhtml"; + scope = "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email"; + userEndpoint = "https://www.googleapis.com/oauth2/v2/userinfo"; + } + + @Override + public BaseApi getApiInstance() { + return GoogleApi20.instance(); + } + + @Override + protected ParsedUserResponse parseUserResponse(String responseBody) { + try ( StringReader rdr = new StringReader(responseBody); + JsonReader jrdr = Json.createReader(rdr) ) { + JsonObject response = jrdr.readObject(); + + AuthenticatedUserDisplayInfo displayInfo = new AuthenticatedUserDisplayInfo( + response.getString("given_name",""), + response.getString("family_name",""), + response.getString("email",""), + "", + "" + ); + return new ParsedUserResponse(displayInfo, response.getString("id")); + } + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/identityproviders/OrcidApi.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/identityproviders/OrcidApi.java new file mode 100644 index 00000000000..3142c8c9b64 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/identityproviders/OrcidApi.java @@ -0,0 +1,58 @@ +package edu.harvard.iq.dataverse.authorization.providers.oauth2.identityproviders; + +import com.github.scribejava.core.builder.api.DefaultApi20; +import com.github.scribejava.core.extractors.OAuth2AccessTokenExtractor; +import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor; +import com.github.scribejava.core.extractors.TokenExtractor; +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.model.Response; +import java.io.IOException; +import java.util.logging.Logger; + +/** + * Adaptor for ORCiD OAuth identity Provider. + * @author michael + */ +public class OrcidApi extends DefaultApi20 { + + /** + * The instance holder pattern allows for lazy creation of the intance. + */ + private static class InstanceHolder { + private static final OrcidApi INSTANCE = new OrcidApi(); + } + + public static OrcidApi instance() { + return InstanceHolder.INSTANCE; + } + + protected OrcidApi(){} + + @Override + public String getAccessTokenEndpoint() { + return "https://orcid.org/oauth/token"; + } + + @Override + protected String getAuthorizationBaseUrl() { + return "https://orcid.org/oauth/authorize"; + } + + @Override + public TokenExtractor getAccessTokenExtractor() { + return OAuth2AccessTokenJsonExtractor.instance(); + } + + static class TE extends OAuth2AccessTokenJsonExtractor { + + @Override + public OAuth2AccessToken extract(String response) { + Logger.getAnonymousLogger().info("Response:"); + Logger.getAnonymousLogger().info(response); + Logger.getAnonymousLogger().info("/Response"); + return super.extract(response); + } + + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/identityproviders/OrcidPublicOAuth2Idp.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/identityproviders/OrcidPublicOAuth2Idp.java new file mode 100644 index 00000000000..0640f5c7c4c --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/identityproviders/OrcidPublicOAuth2Idp.java @@ -0,0 +1,43 @@ +package edu.harvard.iq.dataverse.authorization.providers.oauth2.identityproviders; + +import com.github.scribejava.core.builder.api.BaseApi; +import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.AbstractOAuth2Idp; +import java.util.logging.Logger; + +/** + * + * @author michael + */ +public class OrcidPublicOAuth2Idp extends AbstractOAuth2Idp { + + private static final String API_ENDPOINT = "https://pub.orcid.org/v1.2/"; //https://api.sandbox.orcid.org/"; + + public OrcidPublicOAuth2Idp() { + id = "orcid-pub"; + title = "ORCiD (public api)"; + clientId = "APP-3FSE1LYJI56P5VNJ"; // TODO load from config + clientSecret = "771e0798-0a0a-4748-99f4-a4d8c6faa037"; // TODO load from config + redirectUrl = "http://localhost:8080/oauth2/callback.xhtml"; // TODO load from config + scope = "/read-public"; // might need to add "/authenticate" again +// Sandbox data +// clientId = "APP-HIV99BRM37FSWPH6"; // TODO load from config +// clientSecret = "ee844b70-f223-4f15-9b6f-4991bf8ed7f0"; // TODO load from config +// redirectUrl = "http://localhost:8080/oauth2-callback"; // TODO load from config + userEndpoint = API_ENDPOINT + "orcid_id"; + imageUrl = null; + } + + @Override + public BaseApi getApiInstance() { + return OrcidApi.instance(); + } + + @Override + protected ParsedUserResponse parseUserResponse(String responseBody) { + Logger.getAnonymousLogger().info("ORCiD Response:"); + Logger.getAnonymousLogger().info(responseBody); + return new ParsedUserResponse(new AuthenticatedUserDisplayInfo("fn", "ln", "email", "aff", "pos"), "id in ORCiD"); + } + +} diff --git a/src/main/webapp/oauth-callback.xhtml b/src/main/webapp/oauth-callback.xhtml new file mode 100644 index 00000000000..56b96378cf6 --- /dev/null +++ b/src/main/webapp/oauth-callback.xhtml @@ -0,0 +1,32 @@ + + + + + OAuth callback + + + + + +

+ OAuth2 error +

+

+ Sorry, the identification process did not succeed. +

+

Info:

+

+            #{OAuth2Page.error.message}    
+        
+
Additional Info:
+ Http Return Code: #{OAuth2Page.error.httpReturnCode}

+ Raw message body: +

+            #{OAuth2Page.error.messageBody}
+        
+ +
+ +