package edu.harvard.iq.dataverse.authorization.providers.shib;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import edu.harvard.iq.dataverse.Shib;
import edu.harvard.iq.dataverse.authorization.AuthenticationRequest;
import edu.harvard.iq.dataverse.authorization.AuthenticationResponse;
import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean;
import edu.harvard.iq.dataverse.authorization.exceptions.AuthenticationFailedException;
import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinAuthenticationProvider;
import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUser;
import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean;
import edu.harvard.iq.dataverse.authorization.providers.builtin.PasswordEncryption;
import static edu.harvard.iq.dataverse.authorization.providers.shib.ShibUtil.getRandomUserStatic;
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
import edu.harvard.iq.dataverse.util.BundleUtil;
import edu.harvard.iq.dataverse.util.SystemConfig;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Logger;
import javax.ejb.EJB;
import javax.ejb.EJBException;
import javax.ejb.Stateless;
import javax.inject.Named;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang.StringUtils;
@Named
@Stateless
public class ShibServiceBean {
private static final Logger logger = Logger.getLogger(ShibServiceBean.class.getCanonicalName());
@EJB
AuthenticationServiceBean authSvc;
@EJB
BuiltinUserServiceBean builtinUserService;
@EJB
SystemConfig systemConfig;
@EJB
SettingsServiceBean settingsService;
/**
* "Production" means "don't mess with the HTTP request".
*/
public enum DevShibAccountType {
PRODUCTION,
RANDOM,
TESTSHIB1,
HARVARD1,
HARVARD2,
TWO_EMAILS,
INVALID_EMAIL,
EMAIL_WITH_LEADING_SPACE,
UID_WITH_LEADING_SPACE,
IDENTIFIER_WITH_LEADING_SPACE,
MISSING_REQUIRED_ATTR,
};
public DevShibAccountType getDevShibAccountType() {
DevShibAccountType saneDefault = DevShibAccountType.PRODUCTION;
String settingReturned = settingsService.getValueForKey(SettingsServiceBean.Key.DebugShibAccountType);
logger.fine("setting returned: " + settingReturned);
if (settingReturned != null) {
try {
DevShibAccountType parsedValue = DevShibAccountType.valueOf(settingReturned);
return parsedValue;
} catch (IllegalArgumentException ex) {
logger.info("Couldn't parse value: " + ex + " - returning a sane default: " + saneDefault);
return saneDefault;
}
} else {
logger.fine("Shibboleth dev mode has not been configured. Returning a sane default: " + saneDefault);
return saneDefault;
}
}
/**
* This method exists so developers don't have to run Shibboleth locally.
* You can populate the request with Shibboleth attributes by changing a
* setting like this:
*
* curl -X PUT -d RANDOM
* http://localhost:8080/api/admin/settings/:DebugShibAccountType
*
* When you're done, feel free to delete the setting:
*
* curl -X DELETE
* http://localhost:8080/api/admin/settings/:DebugShibAccountType
*
* Note that setting ShibUseHeaders to true will make this "dev mode" stop
* working.
*/
public void possiblyMutateRequestInDev(HttpServletRequest request) {
switch (getDevShibAccountType()) {
case PRODUCTION:
logger.fine("Request will not be mutated");
break;
case RANDOM:
mutateRequestForDevRandom(request);
break;
case TESTSHIB1:
ShibUtil.mutateRequestForDevConstantTestShib1(request);
break;
case HARVARD1:
ShibUtil.mutateRequestForDevConstantHarvard1(request);
break;
case HARVARD2:
ShibUtil.mutateRequestForDevConstantHarvard2(request);
break;
case TWO_EMAILS:
ShibUtil.mutateRequestForDevConstantTwoEmails(request);
break;
case INVALID_EMAIL:
ShibUtil.mutateRequestForDevConstantInvalidEmail(request);
break;
case EMAIL_WITH_LEADING_SPACE:
ShibUtil.mutateRequestForDevConstantEmailWithLeadingSpace(request);
break;
case UID_WITH_LEADING_SPACE:
ShibUtil.mutateRequestForDevConstantUidWithLeadingSpace(request);
break;
case IDENTIFIER_WITH_LEADING_SPACE:
ShibUtil.mutateRequestForDevConstantIdentifierWithLeadingSpace(request);
break;
case MISSING_REQUIRED_ATTR:
ShibUtil.mutateRequestForDevConstantMissingRequiredAttributes(request);
break;
default:
logger.info("Should never reach here");
break;
}
}
public AuthenticatedUser findAuthUserByEmail(String emailToFind) {
return authSvc.getAuthenticatedUserByEmail(emailToFind);
}
public BuiltinUser findBuiltInUserByAuthUserIdentifier(String authUserIdentifier) {
return builtinUserService.findByUserName(authUserIdentifier);
}
public AuthenticatedUser canLogInAsBuiltinUser(String username, String password) {
logger.fine("checking to see if " + username + " knows the password...");
if (password == null) {
logger.info("password was null");
return null;
}
AuthenticationRequest authReq = new AuthenticationRequest();
/**
* @todo Should this really be coming from a bundle like this? Added
* because that's what BuiltinAuthenticationProvider does.
*/
authReq.putCredential(BundleUtil.getStringFromBundle("login.builtin.credential.usernameOrEmail"), username);
authReq.putCredential(BundleUtil.getStringFromBundle("login.builtin.credential.password"), password);
/**
* @todo Should probably set IP address here.
*/
// authReq.setIpAddress(session.getUser().getRequestMetadata().getIpAddress());
String credentialsAuthProviderId = BuiltinAuthenticationProvider.PROVIDER_ID;
try {
AuthenticatedUser au = authSvc.authenticate(credentialsAuthProviderId, authReq);
logger.fine("User authenticated:" + au.getEmail());
return au;
} catch (AuthenticationFailedException ex) {
logger.info("The username and/or password entered is invalid: " + ex.getResponse().getMessage());
if (AuthenticationResponse.Status.BREAKOUT.equals(ex.getResponse().getStatus())) {
/**
* Note that this "BREAKOUT" status creates PasswordResetData!
* We'll delete it just before blowing away the BuiltinUser in
* AuthenticationServiceBean.convertBuiltInToShib
*/
logger.info("AuthenticationFailedException caught in canLogInAsBuiltinUser: The username and/or password entered is invalid: " + ex.getResponse().getMessage() + " - Maybe the user (" + username + ") hasn't upgraded their password? Checking the old password...");
BuiltinUser builtinUser = builtinUserService.findByUsernameOrEmail(username);
if (builtinUser != null) {
boolean userAuthenticated = PasswordEncryption.getVersion(builtinUser.getPasswordEncryptionVersion()).check(password, builtinUser.getEncryptedPassword());
if (userAuthenticated == true) {
AuthenticatedUser authUser = authSvc.lookupUser(BuiltinAuthenticationProvider.PROVIDER_ID, builtinUser.getUserName());
if (authUser != null) {
return authUser;
} else {
logger.info("canLogInAsBuiltinUser: Couldn't find AuthenticatedUser based on BuiltinUser username " + builtinUser.getUserName());
}
} else {
logger.info("canLogInAsBuiltinUser: User doesn't know old pre-bcrypt password either.");
}
} else {
logger.info("canLogInAsBuiltinUser: Couldn't run `check` because no BuiltinUser found with username " + username);
}
}
return null;
} catch (EJBException ex) {
Throwable cause = ex;
StringBuilder sb = new StringBuilder();
sb.append(ex + " ");
while (cause.getCause() != null) {
cause = cause.getCause();
sb.append(cause.getClass().getCanonicalName() + " ");
sb.append(cause.getMessage()).append(" ");
/**
* @todo Investigate why authSvc.authenticate is throwing
* NullPointerException. If you convert a Shib user to a Builtin
* user, the password may be null.
*/
if (cause instanceof NullPointerException) {
for (int i = 0; i < 2; i++) {
StackTraceElement stacktrace = cause.getStackTrace()[i];
if (stacktrace != null) {
String classCanonicalName = stacktrace.getClass().getCanonicalName();
String methodName = stacktrace.getMethodName();
int lineNumber = stacktrace.getLineNumber();
String error = "at " + stacktrace.getClassName() + "." + stacktrace.getMethodName() + "(" + stacktrace.getFileName() + ":" + lineNumber + ") ";
sb.append(error);
}
}
}
}
logger.info("When trying to validate password, exception calling authSvc.authenticate: " + sb.toString());
return null;
}
}
public String getAffiliation(String shibIdp, DevShibAccountType devShibAccountType) {
JsonArray emptyJsonArray = new JsonArray();
String discoFeedJson = emptyJsonArray.toString();
String discoFeedUrl;
if (devShibAccountType.equals(DevShibAccountType.PRODUCTION)) {
discoFeedUrl = systemConfig.getDataverseSiteUrl() + "/Shibboleth.sso/DiscoFeed";
} else {
String devUrl = "http://localhost:8080/resources/dev/sample-shib-identities.json";
discoFeedUrl = devUrl;
}
logger.fine("Trying to get affiliation from disco feed URL: " + discoFeedUrl);
URL url = null;
try {
url = new URL(discoFeedUrl);
} catch (MalformedURLException ex) {
logger.info(ex.toString());
return null;
}
if (url == null) {
logger.info("url object was null after parsing " + discoFeedUrl);
return null;
}
HttpURLConnection discoFeedRequest = null;
try {
discoFeedRequest = (HttpURLConnection) url.openConnection();
} catch (IOException ex) {
logger.info(ex.toString());
return null;
}
if (discoFeedRequest == null) {
logger.info("disco feed request was null");
return null;
}
try {
discoFeedRequest.connect();
} catch (IOException ex) {
logger.info(ex.toString());
return null;
}
JsonParser jp = new JsonParser();
JsonElement root = null;
try {
root = jp.parse(new InputStreamReader((InputStream) discoFeedRequest.getInputStream()));
} catch (IOException ex) {
logger.info(ex.toString());
return null;
}
if (root == null) {
logger.info("root was null");
return null;
}
JsonArray rootArray = root.getAsJsonArray();
if (rootArray == null) {
logger.info("Couldn't get JSON Array from URL");
return null;
}
discoFeedJson = rootArray.toString();
logger.fine("Dump of disco feed:" + discoFeedJson);
String affiliation = ShibUtil.getDisplayNameFromDiscoFeed(shibIdp, discoFeedJson);
if (affiliation != null) {
return affiliation;
} else {
logger.info("Couldn't find an affiliation from " + shibIdp);
return null;
}
}
private void mutateRequestForDevRandom(HttpServletRequest request) {
Map<String, String> randomUser = getRandomUser();
request.setAttribute(ShibUtil.lastNameAttribute, randomUser.get("lastName"));
request.setAttribute(ShibUtil.firstNameAttribute, randomUser.get("firstName"));
request.setAttribute(ShibUtil.emailAttribute, randomUser.get("email"));
request.setAttribute(ShibUtil.shibIdpAttribute, randomUser.get("idp"));
// eppn
request.setAttribute(ShibUtil.uniquePersistentIdentifier, UUID.randomUUID().toString().substring(0, 8));
}
/**
* For testing, don't expect this to work well.
*/
public Map<String, String> getRandomUser() throws JsonSyntaxException, JsonIOException {
Map<String, String> fakeUser = new HashMap<>();
String sURL = "http://api.randomuser.me/0.8";
URL url = null;
try {
url = new URL(sURL);
} catch (MalformedURLException ex) {
logger.info("Exception: " + ex);
}
HttpURLConnection randomUserRequest = null;
try {
randomUserRequest = (HttpURLConnection) url.openConnection();
} catch (IOException ex) {
logger.info("Exception: " + ex);
}
try {
randomUserRequest.connect();
} catch (IOException ex) {
logger.info("Exception: " + ex);
}
JsonParser jp = new JsonParser();
JsonElement root = null;
try {
root = jp.parse(new InputStreamReader((InputStream) randomUserRequest.getContent()));
} catch (IOException ex) {
logger.info("Exception: " + ex);
}
if (root == null) {
return getRandomUserStatic();
}
JsonObject rootObject = root.getAsJsonObject();
logger.fine(rootObject.toString());
JsonElement results = rootObject.get("results");
logger.fine(results.toString());
JsonElement firstResult = results.getAsJsonArray().get(0);
logger.fine(firstResult.toString());
JsonElement user = firstResult.getAsJsonObject().get("user");
JsonElement username = user.getAsJsonObject().get("username");
JsonElement salt = user.getAsJsonObject().get("salt");
JsonElement email = user.getAsJsonObject().get("email");
JsonElement password = user.getAsJsonObject().get("password");
JsonElement name = user.getAsJsonObject().get("name");
JsonElement firstName = name.getAsJsonObject().get("first");
JsonElement lastName = name.getAsJsonObject().get("last");
String firstNameString = StringUtils.capitalize(firstName.getAsString());
String lastNameString = StringUtils.capitalize(lastName.getAsString());
fakeUser.put("firstName", firstNameString);
fakeUser.put("lastName", lastNameString);
fakeUser.put("displayName", firstNameString + " " + lastNameString);
fakeUser.put("email", email.getAsString());
fakeUser.put("idp", "https://idp." + password.getAsString() + ".com/idp/shibboleth");
fakeUser.put("username", username.getAsString());
fakeUser.put("eppn", salt.getAsString());
return fakeUser;
}
}