Skip to content

Commit

Permalink
IQSS#5991. Refactor OAuth login bean and provider superclass to be co…
Browse files Browse the repository at this point in the history
…mpatible with recent ScribeJava library. Refactored code structure a bit, too.
  • Loading branch information
poikilotherm committed Jul 5, 2019
1 parent 9aa7fe8 commit 1a4e449
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 71 deletions.
@@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -91,35 +94,48 @@ public String toString() {
protected String redirectUrl;
protected String scope;

public abstract BaseApi<OAuth20Service> 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 ) {
Expand Down Expand Up @@ -195,6 +211,8 @@ public void setSubTitle(String subtitle) {
public String getSubTitle() {
return subTitle;
}

public String getScope() { return scope; }

@Override
public int hashCode() {
Expand Down
@@ -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;
Expand All @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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<AbstractOAuth2AuthenticationProvider> oIdp = parseStateFromRequest(req);
Optional<String> 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<String> parseCodeFromRequest(@NotNull HttpServletRequest req) {
String code = req.getParameter("code");
if (code == null || code.trim().isEmpty()) {
try (BufferedReader rdr = req.getReader()) {
StringBuilder sb = new StringBuilder();
Expand All @@ -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<AbstractOAuth2AuthenticationProvider> 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])) {
Expand All @@ -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();
}
}

Expand Down

0 comments on commit 1a4e449

Please sign in to comment.