diff src/main/java/edu/harvard/iq/dataverse/Shib.java @ 10:a50cf11e5178

Rewrite LGDataverse completely upgrading to dataverse4.0
author Zoe Hong <zhong@mpiwg-berlin.mpg.de>
date Tue, 08 Sep 2015 17:00:21 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/edu/harvard/iq/dataverse/Shib.java	Tue Sep 08 17:00:21 2015 +0200
@@ -0,0 +1,956 @@
+package edu.harvard.iq.dataverse;
+
+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.authorization.AuthenticatedUserDisplayInfo;
+import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean;
+import edu.harvard.iq.dataverse.authorization.UserIdentifier;
+import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier;
+import edu.harvard.iq.dataverse.authorization.groups.impl.shib.ShibGroupServiceBean;
+import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUser;
+import edu.harvard.iq.dataverse.authorization.providers.shib.ShibAuthenticationProvider;
+import edu.harvard.iq.dataverse.authorization.providers.shib.ShibServiceBean;
+import edu.harvard.iq.dataverse.authorization.providers.shib.ShibUserNameFields;
+import edu.harvard.iq.dataverse.authorization.providers.shib.ShibUtil;
+import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
+import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
+import edu.harvard.iq.dataverse.util.JsfHelper;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.ejb.EJB;
+import javax.faces.application.FacesMessage;
+import javax.faces.context.ExternalContext;
+import javax.faces.context.FacesContext;
+import javax.faces.view.ViewScoped;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.servlet.http.HttpServletRequest;
+import org.apache.commons.lang.StringUtils;
+
+@ViewScoped
+@Named("Shib")
+public class Shib implements java.io.Serializable {
+
+    private static final Logger logger = Logger.getLogger(Shib.class.getCanonicalName());
+
+    @Inject
+    DataverseSession session;
+
+    @EJB
+    AuthenticationServiceBean authSvc;
+    @EJB
+    ShibServiceBean shibService;
+    @EJB
+    ShibGroupServiceBean shibGroupService;
+    @EJB
+    SettingsServiceBean settingsService;
+    @EJB
+    SystemConfig systemConfig;
+    @EJB
+    DataverseServiceBean dataverseService;
+
+    HttpServletRequest request;
+
+    /**
+     * @todo these are the attributes we are getting from the IdP at
+     * testshib.org. What other attributes should we expect?
+     *
+     * Here is a dump from https://pdurbin.pagekite.me/Shibboleth.sso/Session
+     *
+     * Miscellaneous
+     *
+     * Session Expiration (barring inactivity): 479 minute(s)
+     *
+     * Client Address: 10.0.2.2
+     *
+     * SSO Protocol: urn:oasis:names:tc:SAML:2.0:protocol
+     *
+     * Identity Provider: https://idp.testshib.org/idp/shibboleth
+     *
+     * Authentication Time: 2014-09-12T17:07:36.137Z
+     *
+     * Authentication Context Class:
+     * urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
+     *
+     * Authentication Context Decl: (none)
+     *
+     *
+     *
+     * Attributes
+     *
+     * affiliation: Member@testshib.org;Staff@testshib.org
+     *
+     * cn: Me Myself And I
+     *
+     * entitlement: urn:mace:dir:entitlement:common-lib-terms
+     *
+     * eppn: myself@testshib.org
+     *
+     * givenName: Me Myself
+     *
+     * persistent-id:
+     * https://idp.testshib.org/idp/shibboleth!https://pdurbin.pagekite.me/shibboleth!zylzL+NruovU5OOGXDOL576jxfo=
+     *
+     * sn: And I
+     *
+     * telephoneNumber: 555-5555
+     *
+     * uid: myself
+     *
+     * unscoped-affiliation: Member;Staff
+     */
+    /**
+     * @todo Resolve potential confusing of having attibutes like "eppn" defined
+     * twice in this class.
+     *
+     * This was used early on in development and should be removed at some
+     * point.
+     */
+    @Deprecated
+    List<String> shibAttrs = Arrays.asList(
+            "Shib-Identity-Provider",
+            "uid",
+            "cn",
+            "sn",
+            "givenName",
+            "telephoneNumber",
+            "eppn",
+            "affiliation",
+            "unscoped-affiliation",
+            "entitlement",
+            "persistent-id"
+    );
+
+    List<String> shibValues = new ArrayList<>();
+    /**
+     * @todo make this configurable?
+     */
+    private final String shibIdpAttribute = "Shib-Identity-Provider";
+    /**
+     * @todo Make attribute used (i.e. "eppn") configurable:
+     * https://github.com/IQSS/dataverse/issues/1422
+     *
+     * OR *maybe* we can rely on people installing Dataverse to configure shibd
+     * to always send "eppn" as an attribute, via attribute mappings or what
+     * have you.
+     */
+    private final String uniquePersistentIdentifier = "eppn";
+    private String userPersistentId;
+    private String internalUserIdentifer;
+    private final String usernameAttribute = "uid";
+    private final String displayNameAttribute = "cn";
+    private final String firstNameAttribute = "givenName";
+    private final String lastNameAttribute = "sn";
+    private final String emailAttribute = "mail";
+    AuthenticatedUserDisplayInfo displayInfo;
+    /**
+     * @todo Remove this boolean some day? Now the mockups show a popup. Should
+     * be re-worked. See also the comment about the lack of a Cancel button.
+     */
+    private boolean visibleTermsOfUse;
+    private final String loginpage = "/loginpage.xhtml";
+    private final String identityProviderProblem = "Problem with Identity Provider";
+
+    /**
+     * We only have one field in which to store a unique
+     * useridentifier/persistentuserid so we have to jam the the "entityId" for
+     * a Shibboleth Identity Provider (IdP) and the unique persistent identifier
+     * per user into the same field and a separator between these two would be
+     * nice, in case we ever want to answer questions like "How many users
+     * logged in from Harvard's Identity Provider?".
+     *
+     * A pipe ("|") is used as a separator because it's considered "unwise" to
+     * use in a URL and the "entityId" for a Shibboleth Identity Provider (IdP)
+     * looks like a URL:
+     * http://stackoverflow.com/questions/1547899/which-characters-make-a-url-invalid
+     */
+    private String persistentUserIdSeparator = "|";
+
+    /**
+     * The Shibboleth Identity Provider (IdP), an "entityId" which often but not
+     * always looks like a URL.
+     */
+    String shibIdp;
+    private String builtinUsername;
+    private String builtinPassword;
+    private String existingEmail;
+    private String existingDisplayName;
+    private boolean passwordRejected;
+    private String displayNameToPersist = "(Blank: display name not received from Institution Log In)";
+//    private String firstNameToPersist = "(Blank: first name not received from Institution Log In)";
+//    private String lastNameToPersist = "(Blank: last name not received from Institution Log In)";
+    private String emailToPersist = "(Blank: email received from Institution Log In)";
+    /**
+     * @todo We're not really doing anything with affiliation yet, even though
+     * the mockups show it. The plan is to parse the JSON from
+     * https://dataverse.harvard.edu/Shibboleth.sso/DiscoFeed for example. Check
+     * the "ShibUtil" class
+     */
+    private String affiliationToDisplayAtConfirmation = "Affiliation not provided by institution log in";
+    /**
+     * @todo Once we can persist "position" to the authenticateduser table, we
+     * can revisit this. Maybe we'll use ORCID instead. Dunno.
+     */
+//    private String positionToPersist = "Position not provided by institution log in";
+    /**
+     * @todo localize this
+     */
+    private String friendlyNameForInstitution = "your institution";
+    private State state;
+    private String debugSummary;
+//    private boolean debug = false;
+    private String emailAddress;
+
+    public enum State {
+
+        INIT,
+        REGULAR_LOGIN_INTO_EXISTING_SHIB_ACCOUNT,
+        PROMPT_TO_CREATE_NEW_ACCOUNT,
+        PROMPT_TO_CONVERT_EXISTING_ACCOUNT,
+    };
+
+    public void init() {
+        state = State.INIT;
+        ExternalContext context = FacesContext.getCurrentInstance().getExternalContext();
+        request = (HttpServletRequest) context.getRequest();
+
+        possiblyMutateRequestInDev();
+
+        try {
+            shibIdp = getRequiredValueFromAttribute(shibIdpAttribute);
+        } catch (Exception ex) {
+            /**
+             * @todo is in an antipattern to throw exceptions to control flow?
+             * http://c2.com/cgi/wiki?DontUseExceptionsForFlowControl
+             *
+             * All this exception handling should be handled in the new
+             * ShibServiceBean so it's consistently handled by the API as well.
+             */
+            return;
+        }
+        String shibUserIdentifier;
+        try {
+            shibUserIdentifier = getRequiredValueFromAttribute(uniquePersistentIdentifier);
+        } catch (Exception ex) {
+            return;
+        }
+        String firstName;
+        try {
+            firstName = getRequiredValueFromAttribute(firstNameAttribute);
+        } catch (Exception ex) {
+            return;
+        }
+        String lastName;
+        try {
+            lastName = getRequiredValueFromAttribute(lastNameAttribute);
+        } catch (Exception ex) {
+            return;
+        }
+        ShibUserNameFields shibUserNameFields = ShibUtil.findBestFirstAndLastName(firstName, lastName, null);
+        if (shibUserNameFields != null) {
+            String betterFirstName = shibUserNameFields.getFirstName();
+            if (betterFirstName != null) {
+                firstName = betterFirstName;
+            }
+            String betterLastName = shibUserNameFields.getLastName();
+            if (betterLastName != null) {
+                lastName = betterLastName;
+            }
+        }
+        try {
+            emailAddress = getRequiredValueFromAttribute(emailAttribute);
+        } catch (Exception ex) {
+            String testShibIdpEntityId = "https://idp.testshib.org/idp/shibboleth";
+            if (shibIdp.equals(testShibIdpEntityId)) {
+                logger.info("For " + testShibIdpEntityId + " (which as of this writing doesn't provide the " + emailAttribute + " attribute) setting email address to value of eppn: " + shibUserIdentifier);
+                emailAddress = shibUserIdentifier;
+            } else {
+                // forcing all other IdPs to send us an an email
+                return;
+            }
+        }
+        internalUserIdentifer = generateFriendlyLookingUserIdentifer(usernameAttribute, emailAttribute);
+        logger.info("friendly looking identifer (backend will enforce uniqueness):" + internalUserIdentifer);
+
+        /**
+         * @todo Remove, longer term. For now, commenting out special logic for
+         * always showing Terms of Use for TestShib accounts. The Terms of Use
+         * workflow is captured at
+         * http://datascience.iq.harvard.edu/blog/try-out-single-sign-shibboleth-40-beta
+         */
+//        if (shibIdp.equals("https://idp.testshib.org/idp/shibboleth")) {
+//            StringBuilder sb = new StringBuilder();
+//            String freshNewShibUser = sb.append(userIdentifier).append(UUID.randomUUID()).toString();
+//            logger.info("Will create a new, unique user so the account Terms of Use will be displayed.");
+//            userIdentifier = freshNewShibUser;
+//        }
+        /**
+         * @todo Shouldn't we persist the displayName too? It still exists on
+         * the authenticateduser table.
+         */
+//        String displayName = getDisplayName(displayNameAttribute, firstNameAttribute, lastNameAttribute);
+        String affiliation = getAffiliation();
+        displayInfo = new AuthenticatedUserDisplayInfo(firstName, lastName, emailAddress, affiliation, null);
+
+        userPersistentId = shibIdp + persistentUserIdSeparator + shibUserIdentifier;
+        ShibAuthenticationProvider shibAuthProvider = new ShibAuthenticationProvider();
+        AuthenticatedUser au = authSvc.lookupUser(shibAuthProvider.getId(), userPersistentId);
+        if (au != null) {
+            state = State.REGULAR_LOGIN_INTO_EXISTING_SHIB_ACCOUNT;
+            logger.info("Found user based on " + userPersistentId + ". Logging in.");
+            logger.info("Updating display info for " + au.getName());
+            authSvc.updateAuthenticatedUser(au, displayInfo);
+            logInUserAndSetShibAttributes(au);
+            String prettyFacesHomePageString = getPrettyFacesHomePageString(false);
+            try {
+                FacesContext.getCurrentInstance().getExternalContext().redirect(prettyFacesHomePageString);
+            } catch (IOException ex) {
+                logger.info("Unable to redirect user to homepage at " + prettyFacesHomePageString);
+            }
+        } else {
+            state = State.PROMPT_TO_CREATE_NEW_ACCOUNT;
+            displayNameToPersist = displayInfo.getTitle();
+//            firstNameToPersist = "foo";
+//            lastNameToPersist = "bar";
+            emailToPersist = emailAddress;
+            /**
+             * @todo For Harvard at least, we plan to use "Harvard University"
+             * for affiliation because it's what we get from
+             * https://dataverse.harvard.edu/Shibboleth.sso/DiscoFeed
+             */
+//            affiliationToPersist = "FIXME";
+            /**
+             * @todo for Harvard we plan to use the value(s) from
+             * eduPersonScopedAffiliation which
+             * http://iam.harvard.edu/resources/saml-shibboleth-attributes says
+             * can be One or more of the following values: faculty, staff,
+             * student, affiliate, and member.
+             *
+             * http://dataverse.nl plans to use
+             * urn:mace:dir:attribute-def:eduPersonAffiliation per
+             * http://irclog.iq.harvard.edu/dataverse/2015-02-13#i_16265 . Can
+             * they configure shibd to map eduPersonAffiliation to
+             * eduPersonScopedAffiliation?
+             */
+//            positionToPersist = "FIXME";
+            logger.info("Couldn't find authenticated user based on " + userPersistentId);
+            visibleTermsOfUse = true;
+            /**
+             * Using the email address from the IdP, try to find an existing
+             * user. For TestShib we convert the "eppn" to an email address.
+             *
+             * If found, prompt for password and offer to convert.
+             *
+             * If not found, create a new account. It must be a new user.
+             */
+            String emailAddressToLookUp = emailAddress;
+            if (existingEmail != null) {
+                emailAddressToLookUp = existingEmail;
+            }
+            AuthenticatedUser existingAuthUserFoundByEmail = shibService.findAuthUserByEmail(emailAddressToLookUp);
+            BuiltinUser existingBuiltInUserFoundByEmail = null;
+            if (existingAuthUserFoundByEmail != null) {
+                existingDisplayName = existingAuthUserFoundByEmail.getName();
+                existingBuiltInUserFoundByEmail = shibService.findBuiltInUserByAuthUserIdentifier(existingAuthUserFoundByEmail.getUserIdentifier());
+                if (existingBuiltInUserFoundByEmail != null) {
+                    state = State.PROMPT_TO_CONVERT_EXISTING_ACCOUNT;
+                    existingDisplayName = existingBuiltInUserFoundByEmail.getDisplayName();
+                    debugSummary = "getting username from the builtin user we looked up via email";
+                    builtinUsername = existingBuiltInUserFoundByEmail.getUserName();
+                } else {
+                    debugSummary = "Could not find a builtin account based on the username. Here we should simply create a new Shibboleth user";
+                }
+            } else {
+                debugSummary = "Could not find an auth user based on email address";
+            }
+
+        }
+//        if (debug) {
+//            printAttributes(request);
+//        }
+    }
+
+    /**
+     * @todo Move this to the shib service bean.
+     */
+    private String getAffiliation() {
+        JsonArray emptyJsonArray = new JsonArray();
+        String discoFeedJson = emptyJsonArray.toString();
+        String discoFeedUrl;
+        if (getDevShibAccountType().equals(DevShibAccountType.PRODUCTION)) {
+            discoFeedUrl = systemConfig.getDataverseSiteUrl() + "/Shibboleth.sso/DiscoFeed";
+        } else {
+            String devUrl = "http://localhost:8080/resources/dev/sample-shib-identities.json";
+            discoFeedUrl = devUrl;
+        }
+        logger.info("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) {
+            affiliationToDisplayAtConfirmation = affiliation;
+            friendlyNameForInstitution = affiliation;
+            return affiliation;
+        } else {
+            logger.info("Couldn't find an affiliation from  " + shibIdp);
+            return null;
+        }
+    }
+
+    /**
+     * "Production" means "don't mess with the HTTP request".
+     */
+    public enum DevShibAccountType {
+
+        PRODUCTION,
+        RANDOM,
+        TESTSHIB1,
+        HARVARD1,
+        HARVARD2,
+    };
+
+    private 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
+     */
+    private void possiblyMutateRequestInDev() {
+        switch (getDevShibAccountType()) {
+            case PRODUCTION:
+                logger.fine("Request will not be mutated");
+                break;
+
+            case RANDOM:
+                mutateRequestForDevRandom();
+                break;
+
+            case TESTSHIB1:
+                mutateRequestForDevConstantTestShib1();
+                break;
+
+            case HARVARD1:
+                mutateRequestForDevConstantHarvard1();
+                break;
+
+            case HARVARD2:
+                mutateRequestForDevConstantHarvard2();
+                break;
+
+            default:
+                logger.info("Should never reach here");
+                break;
+        }
+    }
+
+    public String confirmAndCreateAccount() {
+        ShibAuthenticationProvider shibAuthProvider = new ShibAuthenticationProvider();
+        String lookupStringPerAuthProvider = userPersistentId;
+        AuthenticatedUser au = authSvc.createAuthenticatedUser(
+                new UserRecordIdentifier(shibAuthProvider.getId(), lookupStringPerAuthProvider), internalUserIdentifer, displayInfo, true);
+        if (au != null) {
+            logger.info("created user " + au.getIdentifier());
+        } else {
+            logger.info("couldn't create user " + userPersistentId);
+        }
+        logInUserAndSetShibAttributes(au);
+        return getPrettyFacesHomePageString(true);
+    }
+
+    public String confirmAndConvertAccount() {
+        visibleTermsOfUse = false;
+        ShibAuthenticationProvider shibAuthProvider = new ShibAuthenticationProvider();
+        String lookupStringPerAuthProvider = userPersistentId;
+        UserIdentifier userIdentifier = new UserIdentifier(lookupStringPerAuthProvider, internalUserIdentifer);
+        logger.info("builtin username: " + builtinUsername);
+        AuthenticatedUser builtInUserToConvert = shibService.canLogInAsBuiltinUser(builtinUsername, builtinPassword);
+        if (builtInUserToConvert != null) {
+            AuthenticatedUser au = authSvc.convertBuiltInToShib(builtInUserToConvert, shibAuthProvider.getId(), userIdentifier);
+            if (au != null) {
+                authSvc.updateAuthenticatedUser(au, displayInfo);
+                logInUserAndSetShibAttributes(au);
+                debugSummary = "Local account validated and successfully converted to a Shibboleth account. The old account username was " + builtinUsername;
+                JsfHelper.addSuccessMessage("Your Dataverse account is now associated with your institutional account.");
+                return getPrettyFacesHomePageString(true);
+            } else {
+                debugSummary = "Local account validated but unable to convert to Shibboleth account.";
+            }
+        } else {
+            passwordRejected = true;
+            debugSummary = "Username/password combination for local account was invalid";
+        }
+        return null;
+    }
+
+    private void logInUserAndSetShibAttributes(AuthenticatedUser au) {
+        au.setShibIdentityProvider(shibIdp);
+        session.setUser(au);
+    }
+
+    /**
+     * @todo The mockups show a Cancel button but because we're using the
+     * "requiredCheckboxValidator" you are forced to agree to Terms of Use
+     * before clicking Cancel! Argh! The mockups show how we want to display
+     * Terms of Use in a popup anyway so this should all be re-done. No time
+     * now. Here's the mockup:
+     * https://iqssharvard.mybalsamiq.com/projects/loginwithshibboleth-version3-dataverse40/Dataverse%20Account%20III%20-%20Agree%20Terms%20of%20Use
+     */
+    public String cancel() {
+        return loginpage + "?faces-redirect=true";
+    }
+
+    public List<String> getShibValues() {
+        return shibValues;
+    }
+
+//    private void printAttributes(HttpServletRequest request) {
+//        for (String attr : shibAttrs) {
+//
+//            /**
+//             * @todo explain in Installers Guide that in order for these
+//             * attributes to be found attributePrefix="AJP_" must be added to
+//             * /etc/shibboleth/shibboleth2.xml like this:
+//             *
+//             * <ApplicationDefaults entityID="https://dataverse.org/shibboleth"
+//             * REMOTE_USER="eppn" attributePrefix="AJP_">
+//             *
+//             */
+//            Object attrObject = request.getAttribute(attr);
+//            if (attrObject != null) {
+//                shibValues.add(attr + ": " + attrObject.toString());
+//            }
+//        }
+//        logger.info("shib values: " + shibValues);
+//    }
+    /**
+     * @return The value of a Shib attribute (if non-empty) or null.
+     */
+    private String getValueFromAttribute(String attribute) {
+        Object attributeObject = request.getAttribute(attribute);
+        if (attributeObject != null) {
+            String attributeValue = attributeObject.toString();
+            if (!attributeValue.isEmpty()) {
+                return attributeValue;
+            }
+        }
+        return null;
+    }
+
+    private String getRequiredValueFromAttribute(String attribute) throws Exception {
+        Object attributeObject = request.getAttribute(attribute);
+        if (attributeObject == null) {
+            String msg = " the attribute \"" + attribute + "\" was null. Please contact support.";
+            logger.info(msg);
+            FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, identityProviderProblem, msg));
+            throw new Exception(msg);
+        }
+        String attributeValue = attributeObject.toString();
+        if (attributeValue.isEmpty()) {
+            throw new Exception(attribute + " was empty");
+        }
+        return attributeValue;
+    }
+
+    /**
+     * @todo Move logic to ShibServiceBean
+     */
+    private String generateFriendlyLookingUserIdentifer(String usernameNameAttribute, String emailAttribute) {
+        Object usernameObject = request.getAttribute(usernameNameAttribute);
+        if (usernameObject != null) {
+            String userIdentifier = usernameObject.toString();
+            if (!userIdentifier.isEmpty()) {
+                return userIdentifier;
+            }
+        } else {
+            logger.info("username attribute not sent by IdP");
+        }
+        Object emailObject = request.getAttribute(emailAttribute);
+        if (emailObject != null) {
+            String email = emailObject.toString();
+            if (!email.isEmpty()) {
+                /**
+                 * @todo Just grab the first part of the email
+                 */
+                String[] parts = email.split("@");
+                try {
+                    String firstPart = parts[0];
+                    return firstPart;
+                } catch (ArrayIndexOutOfBoundsException ex) {
+                    logger.info("odd email address. no @ sign: " + email);
+                }
+            }
+        } else {
+            logger.info("email attribute not sent by IdP");
+        }
+        logger.info("the best we can do is generate a random UUID");
+        return UUID.randomUUID().toString();
+    }
+
+    /**
+     * @return The best display name we can retrieve or construct based on
+     * attributes received from Shibboleth. Shouldn't be null, maybe "Unknown"
+     *
+     * @deprecated AuthenticatedUserDisplayInfo has no place for a display name.
+     */
+    @Deprecated
+    private String getDisplayName(String displayNameAttribute, String firstNameAttribute, String lastNameAttribute) {
+        Object displayNameObject = request.getAttribute(displayNameAttribute);
+        if (displayNameObject != null) {
+            String displayName = displayNameObject.toString();
+            if (!displayName.isEmpty()) {
+                return displayName;
+            } else {
+                return getDisplayNameFromFirstNameLastName(firstNameAttribute, lastNameAttribute);
+            }
+        } else {
+            return getDisplayNameFromFirstNameLastName(firstNameAttribute, lastNameAttribute);
+        }
+    }
+
+    /**
+     * @return First name plus last name if available, just first name or just
+     * last name or "Unknown".
+     *
+     * @deprecated AuthenticatedUserDisplayInfo has no place for a display name.
+     */
+    @Deprecated
+    private String getDisplayNameFromFirstNameLastName(String firstNameAttribute, String lastNameAttribute) {
+        /**
+         * @todo Should the first name attribute be required?
+         */
+        String firstName = getValueFromAttribute(firstNameAttribute);
+        /**
+         * @todo Should the last name attribute be required?
+         */
+        String lastName = getValueFromAttribute(lastNameAttribute);
+        if (firstName != null && lastName != null) {
+            return firstName + " " + lastName;
+        } else if (firstName != null) {
+            return firstName;
+        } else if (lastName != null) {
+            return lastName;
+        } else {
+            return "Unknown";
+        }
+    }
+
+    public String getRootDataverseAlias() {
+        Dataverse rootDataverse = dataverseService.findRootDataverse();
+        if (rootDataverse != null) {
+            String rootDvAlias = rootDataverse.getAlias();
+            if (rootDvAlias != null) {
+                return rootDvAlias;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @param includeFacetDashRedirect if true, include "faces-redirect=true" in
+     * the string
+     *
+     * @todo Once https://github.com/IQSS/dataverse/issues/1519 is done, revisit
+     * this method and have the home page be "/" rather than "/dataverses/root".
+     *
+     * @todo Like builtin users, Shibboleth should benefit from redirectPage
+     * logic per https://github.com/IQSS/dataverse/issues/1551
+     */
+    public String getPrettyFacesHomePageString(boolean includeFacetDashRedirect) {
+        String plainHomepageString = "/dataverse.xhtml";
+        String rootDvAlias = getRootDataverseAlias();
+        if (includeFacetDashRedirect) {
+            if (rootDvAlias != null) {
+                return plainHomepageString + "?alias=" + rootDvAlias + "&faces-redirect=true";
+            } else {
+                return plainHomepageString + "?faces-redirect=true";
+            }
+        } else {
+            if (rootDvAlias != null) {
+                /**
+                 * @todo Is there a constant for "/dataverse/" anywhere? I guess
+                 * we'll just hard-code it here.
+                 */
+                return "/dataverse/" + rootDvAlias;
+            } else {
+                return plainHomepageString;
+            }
+        }
+    }
+
+    public boolean isDebug() {
+        return systemConfig.isDebugEnabled();
+    }
+
+    public boolean isInit() {
+        return state.equals(State.INIT);
+    }
+
+    public boolean isOfferToCreateNewAccount() {
+        return state.equals(State.PROMPT_TO_CREATE_NEW_ACCOUNT);
+    }
+
+    public boolean isOfferToConvertExistingAccount() {
+        return state.equals(State.PROMPT_TO_CONVERT_EXISTING_ACCOUNT);
+    }
+
+    public String getDisplayNameToPersist() {
+        return displayNameToPersist;
+    }
+
+//    public String getFirstNameToPersist() {
+//        return firstNameToPersist;
+//    }
+//    public String getLastNameToPersist() {
+//        return lastNameToPersist;
+//    }
+    public String getEmailToPersist() {
+        return emailToPersist;
+    }
+
+    public String getAffiliationToDisplayAtConfirmation() {
+        return affiliationToDisplayAtConfirmation;
+    }
+
+//    public String getPositionToPersist() {
+//        return positionToPersist;
+//    }
+    public String getExistingEmail() {
+        return existingEmail;
+    }
+
+    public void setExistingEmail(String existingEmail) {
+        this.existingEmail = existingEmail;
+    }
+
+    public String getExistingDisplayName() {
+        return existingDisplayName;
+    }
+
+    public boolean isPasswordRejected() {
+        return passwordRejected;
+    }
+
+    public String getFriendlyNameForInstitution() {
+        return friendlyNameForInstitution;
+    }
+
+    public void setFriendlyNameForInstitution(String friendlyNameForInstitution) {
+        this.friendlyNameForInstitution = friendlyNameForInstitution;
+    }
+
+    public State getState() {
+        return state;
+    }
+
+    public boolean isVisibleTermsOfUse() {
+        return visibleTermsOfUse;
+    }
+
+    public String getBuiltinUsername() {
+        return builtinUsername;
+    }
+
+    public void setBuiltinUsername(String builtinUsername) {
+        this.builtinUsername = builtinUsername;
+    }
+
+    public String getBuiltinPassword() {
+        return builtinPassword;
+    }
+
+    public void setBuiltinPassword(String builtinPassword) {
+        this.builtinPassword = builtinPassword;
+    }
+
+    public String getDebugSummary() {
+        return debugSummary;
+    }
+
+    public void setDebugSummary(String debugSummary) {
+        this.debugSummary = debugSummary;
+    }
+
+    private void mutateRequestForDevRandom() throws JsonSyntaxException, JsonIOException {
+        // set *something*, at least, even if it's just shortened UUIDs
+//        for (String attr : shibAttrs) {
+        // in dev we don't care if a new, random user is created each time
+//            request.setAttribute(attr, UUID.randomUUID().toString().substring(0, 8));
+//        }
+
+        String sURL = "http://api.randomuser.me";
+        URL url = null;
+        try {
+            url = new URL(sURL);
+        } catch (MalformedURLException ex) {
+            Logger.getLogger(Shib.class.getName()).log(Level.SEVERE, null, ex);
+        }
+        HttpURLConnection randomUserRequest = null;
+        try {
+            randomUserRequest = (HttpURLConnection) url.openConnection();
+        } catch (IOException ex) {
+            Logger.getLogger(Shib.class.getName()).log(Level.SEVERE, null, ex);
+        }
+        try {
+            randomUserRequest.connect();
+        } catch (IOException ex) {
+            Logger.getLogger(Shib.class.getName()).log(Level.SEVERE, null, ex);
+        }
+
+        JsonParser jp = new JsonParser(); //from gson
+        JsonElement root = null;
+        try {
+            root = jp.parse(new InputStreamReader((InputStream) randomUserRequest.getContent())); //convert the input stream to a json element
+        } catch (IOException ex) {
+            Logger.getLogger(Shib.class.getName()).log(Level.SEVERE, null, ex);
+        }
+        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 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");
+        /**
+         * @todo Does Harvard really send displayName? At one point they didn't.
+         * Let's simulate the non-sending of displayName here.
+         */
+//        request.setAttribute(displayNameAttribute, StringUtils.capitalise(firstName.getAsString()) + " " + StringUtils.capitalise(lastName.getAsString()));
+        request.setAttribute(lastNameAttribute, StringUtils.capitalise(lastName.getAsString()));
+        request.setAttribute(firstNameAttribute, StringUtils.capitalise(firstName.getAsString()));
+        request.setAttribute(emailAttribute, email.getAsString());
+        // random IDP
+        request.setAttribute(shibIdpAttribute, "https://idp." + password.getAsString() + ".com/idp/shibboleth");
+        /**
+         * Harvard's IdP doesn't send a username so let's test without it by
+         * commenting it out here.
+         */
+//        request.setAttribute(usernameAttribute, username.getAsString());
+        // eppn
+        request.setAttribute(uniquePersistentIdentifier, UUID.randomUUID().toString().substring(0, 8));
+    }
+
+    private void mutateRequestForDevConstantTestShib1() {
+        request.setAttribute(shibIdpAttribute, "https://idp.testshib.org/idp/shibboleth");
+        // the TestShib "eppn" looks like an email address
+        request.setAttribute(uniquePersistentIdentifier, "saml@testshib.org");
+//        request.setAttribute(displayNameAttribute, "Sam El");
+        request.setAttribute(firstNameAttribute, "Samuel;Sam");
+        request.setAttribute(lastNameAttribute, "El");
+        // TestShib doesn't send "mail" attribute so let's mimic that.
+//        request.setAttribute(emailAttribute, "saml@mailinator.com");
+        request.setAttribute(usernameAttribute, "saml");
+    }
+
+    private void mutateRequestForDevConstantHarvard1() {
+        request.setAttribute(shibIdpAttribute, "https://fed.huit.harvard.edu/idp/shibboleth");
+        request.setAttribute(uniquePersistentIdentifier, "constantHarvard");
+//        request.setAttribute(displayNameAttribute, "John Harvard");
+        request.setAttribute(firstNameAttribute, "John");
+        request.setAttribute(lastNameAttribute, "Harvard");
+        request.setAttribute(emailAttribute, "jharvard@mailinator.com");
+        request.setAttribute(usernameAttribute, "jharvard");
+    }
+
+    private void mutateRequestForDevConstantHarvard2() {
+        request.setAttribute(shibIdpAttribute, "https://fed.huit.harvard.edu/idp/shibboleth");
+        request.setAttribute(uniquePersistentIdentifier, "constantHarvard2");
+//        request.setAttribute(displayNameAttribute, "Grace Hopper");
+        request.setAttribute(firstNameAttribute, "Grace");
+        request.setAttribute(lastNameAttribute, "Hopper");
+        request.setAttribute(emailAttribute, "ghopper@mailinator.com");
+        request.setAttribute(usernameAttribute, "ghopper");
+    }
+
+}