org.wyona.yanel.impl.resources.forgotpw.ForgotPassword.java Source code

Java tutorial

Introduction

Here is the source code for org.wyona.yanel.impl.resources.forgotpw.ForgotPassword.java

Source

/*
 * Copyright 2009 Wyona
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.wyona.org/licenses/APACHE-LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.wyona.yanel.impl.resources.forgotpw;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.net.URL;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import javax.management.timer.Timer;
import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.validator.EmailValidator;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.wyona.commons.xml.XMLHelper;
import org.wyona.security.core.api.User;
import org.wyona.yanel.impl.resources.BasicXMLResource;
import org.wyona.yarep.core.Node;
import org.wyona.yarep.core.NodeType;

/**
 * This resource is responsible for managing the forgot password functionality.
 * The following constant control the flow with the UI.
 * {@value #SUBMITFORGOTPASSWORD}  is passed when the user clicks on the first screen to submit email.
 *
 * {@value #SUBMITNEWPW}  is passed when the user enter the new password and submits the form.
 *
 * If the query string has pwresetid then we know that the user clicked on the link sent via email.
 */
public class ForgotPassword extends BasicXMLResource {

    private static Logger log = LogManager.getLogger(ForgotPassword.class);
    private long totalValidHrs;

    private static final String PW_RESET_ID = "pwresetid";
    private static final String SUBMITFORGOTPASSWORD = "submitForgotPW";
    private static final String SUBMITNEWPW = "submitNewPW";
    private static final String NAMESPACE = "http://www.wyona.org/yanel/1.0";

    private static final String SMTP_HOST_PROPERTY_NAME = "smtpHost";
    private static final String SMTP_PORT_PROPERTY_NAME = "smtpPort";

    private static final String HOURS_VALID_PROPERTY_NAME = "num-hrs-valid";
    private static final long DEFAULT_TOTAL_VALID_HRS = 24L;

    private static final String UUID_TAG = "guid";
    private static final String UUID_PARAM = "guid";

    /**
     * This is the main method that handles all view request. The first time the request
     * is made to enter the data.
     */
    @Override
    protected InputStream getContentXML(String viewId) throws Exception {
        HttpServletRequest request = getEnvironment().getRequest();

        try {
            String hrsValid = getResourceConfigProperty(HOURS_VALID_PROPERTY_NAME);
            if (hrsValid != null && !hrsValid.equals("")) {
                totalValidHrs = Long.parseLong(hrsValid);
            } else {
                totalValidHrs = DEFAULT_TOTAL_VALID_HRS;
            }
        } catch (Exception ex) {
            log.error("num-hrs-valid flag not properly set: " + ex, ex);
            totalValidHrs = DEFAULT_TOTAL_VALID_HRS;
        }
        Document adoc = XMLHelper.createDocument(NAMESPACE, "yanel-forgotpw");

        try {
            processUserAction(request, adoc);
        } catch (Exception e) {
            log.error(e, e);
            Element exceptionEl = (Element) adoc.getDocumentElement().appendChild(adoc.createElement("exception"));
            exceptionEl.setTextContent(getStackTrace(e));
        }

        DOMSource source = new DOMSource(adoc);
        StringWriter xmlAsWriter = new StringWriter();
        StreamResult result = new StreamResult(xmlAsWriter);
        TransformerFactory.newInstance().newTransformer().transform(source, result);
        // write changes
        ByteArrayInputStream inputStream = new ByteArrayInputStream(xmlAsWriter.toString().getBytes("UTF-8"));
        return inputStream;
    }

    /**
     * Get stack trace of exception
     * @param e The exception to handle.
     */
    private String getStackTrace(Exception e) {
        java.io.StringWriter sw = new java.io.StringWriter();
        e.printStackTrace(new java.io.PrintWriter(sw));
        return sw.toString();
    }

    /**
     * @param adoc XML DOM Document which will be used to generate response
     */
    private void processUserAction(HttpServletRequest request, Document adoc) throws Exception {
        String action = determineAction(request);
        log.debug("action performed: " + action);

        Element rootElement = adoc.getDocumentElement();
        String resetPasswordRequestUUID = getForgotPasswordRequestUUID(request);
        if (action.equals(SUBMITFORGOTPASSWORD)) {
            String email = request.getParameter("email");
            Element messageElement = (Element) rootElement
                    .appendChild(adoc.createElementNS(NAMESPACE, "show-message"));
            Element cpeElement = (Element) rootElement
                    .appendChild(adoc.createElementNS(NAMESPACE, "change-password-email"));
            cpeElement.setAttribute("submitted-email", email);
            try {
                String uuid = generateForgotPasswordRequest(email, messageElement, cpeElement);
                if (uuid != null) {
                    messageElement.setTextContent(
                            "Password change request was successful. Please check your email for further instructions on how to complete your request.");
                    cpeElement.setAttribute("status", "200");
                    if (getResourceConfigProperty("include-change-password-link") != null
                            && getResourceConfigProperty("include-change-password-link").equals("true")) {
                        log.warn(
                                "Change password link will be part of response! Because of security reasons this should only be done for development or testing environments.");
                        cpeElement.setAttribute("change-password-link", getURL(uuid));
                        cpeElement.setAttribute("uuid", uuid);
                    }
                } else {
                    log.warn("No forgot password request UUID!");
                }
            } catch (Exception e) {
                log.warn(e.getMessage());
                messageElement.setTextContent(e.getMessage());
                cpeElement.setAttribute("status", "400");

                Element exceptionEl = (Element) rootElement
                        .appendChild(adoc.createElementNS(NAMESPACE, "exception"));
                exceptionEl.setTextContent(getStackTrace(e));
            }

        } else if (resetPasswordRequestUUID != null && !resetPasswordRequestUUID.equals("")
                && !action.equals(SUBMITNEWPW)) {
            log.debug("Reset password request UUID: " + resetPasswordRequestUUID);
            if (!existsRequestUUID(resetPasswordRequestUUID)) {
                String errorMsg = "Unable to find forgot password request with request UUID '"
                        + resetPasswordRequestUUID
                        + "'. Maybe request UUID has a typo or request has expired or got deleted by administrator. Please try again.";
                log.warn(errorMsg);
                Element statusElement = (Element) rootElement
                        .appendChild(adoc.createElementNS(NAMESPACE, "show-message"));
                statusElement.setTextContent(errorMsg);
            } else {
                Element requestpwElement = (Element) rootElement
                        .appendChild(adoc.createElementNS(NAMESPACE, "requestnewpw"));
                Element guidElement = (Element) requestpwElement
                        .appendChild(adoc.createElementNS(NAMESPACE, UUID_TAG));
                guidElement.setTextContent(resetPasswordRequestUUID);
            }
        } else if (action.equals(SUBMITNEWPW)) {
            Element messageElement = (Element) rootElement
                    .appendChild(adoc.createElementNS(NAMESPACE, "show-message")); // INFO: We need to keep this element for backwards compatibility reasons!
            Element pwUpdateElement = (Element) rootElement
                    .appendChild(adoc.createElementNS(NAMESPACE, "password-update")); // INFO: We have introduced this element, because the "show-message" element is ambiguous, because it is also used while generating a password change request
            pwUpdateElement.setAttribute(UUID_TAG, request.getParameter(UUID_PARAM));

            try {
                updatePassword(request.getParameter("newPassword"), request.getParameter("newPasswordConfirmation"),
                        request.getParameter(UUID_PARAM));
                messageElement.setTextContent(
                        "Password has been successfully reset. Please login with your new password.");
                pwUpdateElement.setAttribute("status", "200");
            } catch (Exception e) {
                log.warn(e.getMessage());
                messageElement.setTextContent(e.getMessage());
                pwUpdateElement.setAttribute("status", "400");
            }
        } else {
            log.debug("default handler");
            String smtpEmailServer = getResourceConfigProperty(SMTP_HOST_PROPERTY_NAME);
            String smtpEmailServerPort = getResourceConfigProperty(SMTP_PORT_PROPERTY_NAME);
            if ((smtpEmailServer != null && smtpEmailServerPort != null)
                    || (getYanel().getSMTPHost() != null && getYanel().getSMTPPort() >= 0)) {
                String from = getFromEmail();
                if (from != null) {
                    Element requestEmailElement = (Element) rootElement
                            .appendChild(adoc.createElementNS(NAMESPACE, "requestemail")); // INFO: A phone application might have cached the email address and hence wants to auto-complete the form...
                    String emailAddress = getEnvironment().getRequest().getParameter("email");
                    if (emailAddress != null) {
                        requestEmailElement.appendChild(adoc.createTextNode(emailAddress));
                    }
                } else {
                    Element exceptionElement = (Element) rootElement
                            .appendChild(adoc.createElementNS(NAMESPACE, "exception"));
                    String resConfigFilename = "global-resource-configs/user-forgot-pw_yanel-rc.xml";
                    if (getConfiguration().getNode() != null) {
                        resConfigFilename = getConfiguration().getNode().getPath();
                    }
                    exceptionElement.setTextContent(
                            "The FROM address has not been configured yet. Please make sure to configure the FROM address inside the resource configuration '"
                                    + resConfigFilename + "' (either globally or per realm)");
                }
            } else {
                Element exceptionElement = (Element) rootElement
                        .appendChild(adoc.createElementNS(NAMESPACE, "exception"));
                String resConfigFilename = "global-resource-configs/user-forgot-pw_yanel-rc.xml";
                if (getConfiguration().getNode() != null) {
                    resConfigFilename = getConfiguration().getNode().getPath();
                }
                exceptionElement.setTextContent(
                        "SMTP host/port has not been configured yet. Please make sure to configure the various smtp properties at: "
                                + resConfigFilename + " (Or within WEB-INF/classes/yanel.xml)");
            }
        }
    }

    /**
     * Check whether password forgot request UUID still exists (For example it might has expired)
     * @param uuid Password forgot request UUID
     */
    protected boolean existsRequestUUID(String uuid) throws Exception {
        User usr = getUserForRequest(uuid, totalValidHrs);
        if (usr != null) {
            return true;
        } else {
            log.warn("Password forgot request with UUID '" + uuid + "' does not exist (maybe has expired)");
            return false;
        }
    }

    /**
     * Get user for a specific request ID
     * @param requestID Request ID
     */
    private User getUserForRequest(String requestID, long duration_hour) throws Exception {
        log.debug("Find user for request with ID: " + requestID);
        if (getRealm().getRepository().existsNode(getPersistentRequestPath(requestID))) {
            Node requestNode = getRealm().getRepository().getNode(getPersistentRequestPath(requestID));

            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            DocumentBuilder db = null;
            db = dbf.newDocumentBuilder();
            Document doc = db.parse(requestNode.getInputStream());
            Element rootElem = doc.getDocumentElement();
            String userid = rootElem.getAttribute("id");

            Element requestTimeElem = org.wyona.commons.xml.XMLHelper.getChildElements(rootElem, "request-time",
                    null)[0];
            long savedDateTime = new Long(requestTimeElem.getAttribute("millis")).longValue();
            log.warn("Request time: " + savedDateTime);
            if (isExpired(savedDateTime, duration_hour)) {
                log.warn("Request is expired");
                return null;
            }

            return realm.getIdentityManager().getUserManager().getUser(userid);
        } else {
            log.warn("No such request ID: " + requestID);
            return null;
        }
    }

    /**
     * Check if request is expired
     */
    private boolean isExpired(long starDT, long duration_hour) throws Exception {
        long currentDT = new Date().getTime();
        long expireTime = starDT + duration_hour * Timer.ONE_HOUR;

        return (expireTime < currentDT);
    }

    private String getTextValue(Element ele, String tagName) throws Exception {
        String textVal = null;
        NodeList nl = ele.getElementsByTagName(tagName);
        if (nl != null && nl.getLength() > 0) {
            Element el = (Element) nl.item(0);
            textVal = el.getFirstChild().getNodeValue();
        }

        return textVal;
    }

    /* Determine the requested view: defaultView, submitProfile,
    * submitPassword,submitGroup, submitDelete
    *
    * @param request
    * @return name of the desired view
    */
    private String determineAction(HttpServletRequest request) throws Exception {
        boolean submit = false;
        String action = "defaultView";

        Enumeration<?> enumeration = request.getParameterNames();
        while (enumeration.hasMoreElements() && !submit) {
            action = enumeration.nextElement().toString();
            if (action.startsWith("submit")) {
                submit = true;
            }
        }
        return action;
    }

    /**
     * Generate password change request
     * @param email E-Mail address of user
     * @return request UUID if user with specific email address exists and email was sent, return null or throw an exception otherwise
     */
    private String generateForgotPasswordRequest(String email, Element messageEl, Element cpeElement)
            throws Exception {
        String exceptionMsg;
        if (email == null || ("").equals(email)) {
            exceptionMsg = "E-mail address is empty.";
        } else if (!EmailValidator.getInstance().isValid(email)) {
            exceptionMsg = email + " is not a valid E-mail address.";
        } else {
            User user = getUser(email);
            if (user == null) {
                exceptionMsg = "Unable to find user for email address '" + email + "'.";
            } else {
                String uuid = UUID.randomUUID().toString();
                uuid = sendEmail(uuid, user.getEmail());
                if (uuid != null) {
                    ResetPWExpire pwexp = new ResetPWExpire(user.getID(), new Date().getTime(), uuid,
                            user.getEmail());
                    writeXMLOutput(getPersistentRequestPath(uuid), generateXML(pwexp));
                    return uuid;
                } else {
                    exceptionMsg = "No forgot password request UUID was generated (please check log file to check what went wrong)";
                }
            }
        }
        log.warn(exceptionMsg);
        messageEl.setTextContent(exceptionMsg);
        cpeElement.setAttribute("status", "400");
        return null;
    }

    /**
     * Get user which is associated with an email address
     * @param email Email address of user
     * @return user associated with email
     */
    protected User getUser(String email) throws Exception {
        java.util.Iterator<User> it = realm.getIdentityManager().getUserManager().getUsers(email);
        if (it.hasNext()) {
            log.warn("DEBUG: Found at least one user for email '" + email + "' ...");
            User uniqueUser = null;
            while (it.hasNext()) {
                User user = (User) it.next();
                if (email.equals(user.getEmail())) {
                    if (uniqueUser != null) {
                        log.warn("There seems to be more than one user with the email '" + email + "'!");
                    }
                    uniqueUser = user;
                }
            }
            if (uniqueUser != null) {
                return uniqueUser;
            }
        }
        log.warn("No user found with email '" + email + "'!");
        return null;

        /*
                log.warn("TODO: Checking every user by her/his email does not scale!");
                User[] userList = realm.getIdentityManager().getUserManager().getUsers(true);
                for(int i=0; i< userList.length; i++) {
        if (userList[i].getEmail().equals(email)) {
            return userList[i];
        }
                }
                log.warn("No user found with email addres '" + email + "'!'!");
                return null;
        */
    }

    /**
     * Generate XML containing request information which will be saved persistently
     */
    private String generateXML(ResetPWExpire resetObj) throws Exception {
        org.w3c.dom.Document adoc = org.wyona.commons.xml.XMLHelper.createDocument(NAMESPACE, "user");
        Element userElement = adoc.getDocumentElement();
        userElement.setAttribute("id", resetObj.getUserId());

        Element emailElement = (Element) userElement.appendChild(adoc.createElement("email"));
        emailElement.setTextContent(resetObj.getEmail());

        Element startTimeElement = (Element) userElement.appendChild(adoc.createElement("request-time"));
        startTimeElement.setAttribute("millis", Long.toString(resetObj.getDateTime()));
        java.text.DateFormat dateFormat = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
        startTimeElement.setTextContent(dateFormat.format(resetObj.getDateTime()));

        Element guidElement = (Element) userElement.appendChild(adoc.createElement(UUID_TAG));
        guidElement.setTextContent(resetObj.getGuid());

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        TransformerFactory factory = TransformerFactory.newInstance();
        Transformer t = factory.newTransformer(); // identity transform
        DOMSource source = new DOMSource(adoc);
        StreamResult result = new StreamResult(baos);
        t.transform(source, result);

        return baos.toString();
    }

    /**
     * Write reset password request into Yarep node
     * @param path Yarep node path
     * @param content XML content
     */
    private void writeXMLOutput(String path, String content) throws Exception {
        Node fileToStore = null;
        if (getRealm().getRepository().existsNode(path)) {
            fileToStore = getRealm().getRepository().getNode(path);
        } else {
            fileToStore = getRealm().getRepository().getRootNode().addNode(path, NodeType.RESOURCE);
        }
        InputStream in = new ByteArrayInputStream(content.getBytes());
        OutputStream out = fileToStore.getOutputStream();
        byte buffer[] = new byte[8192];
        int bytesRead;
        while ((bytesRead = in.read(buffer)) != -1) {
            out.write(buffer, 0, bytesRead);
        }
        out.close();
        in.close();
    }

    /**
     * Validate and change new user password.
     * @param newPassword New password
     * @param confirmedPassword New password confirmed
     * @param uuid UUID of forgot password request
     * @return "success" if validation and updating new user password was successful, otherwise return exception message
     */
    protected void updatePassword(String newPassword, String confirmedPassword, String uuid) throws Exception {
        if (newPassword == null || newPassword.length() == 0) {
            String exceptionMsg = "No password was submitted!";
            log.warn(exceptionMsg);
            throw new Exception(exceptionMsg);
        }

        // INFO: The confirmed password is optional, but if provided, then it will be compared
        if (confirmedPassword != null && !newPassword.equals(confirmedPassword)) {
            String exceptionMsg = "Password and confirmed password do not match!";
            log.warn(exceptionMsg);
            throw new Exception(exceptionMsg);
        }

        User user = getUserForRequest(uuid, totalValidHrs);
        if (user != null) {
            user.setPassword(newPassword);
            user.save();
            getRealm().getRepository().delete(new org.wyona.yarep.core.Path(getPersistentRequestPath(uuid))); // DEPRECATED
            //TODO: YarepUtil.deleteNode(getRealm().getRepository(), getPersistentRequestPath(uuid));
        } else {
            throw new Exception("Unable to find user for password reset UUID: " + uuid);
        }
    }

    /**
     * @see
     */
    @Override
    public boolean exists() throws Exception {
        return true;
    }

    /**
     * Get forgot password URL which will be sent via E-Mail (also see YanelServlet#getRequestURLQS(HttpServletRequest, String, boolean))
     * @param uuid UUID of forgot password request
     */
    private String getURL(String uuid) throws Exception {
        //https://192.168.1.69:8443/yanel" + request.getServletPath().toString()
        URL url = new URL(request.getRequestURL().toString());
        org.wyona.yanel.core.map.Realm realm = getRealm();
        if (realm.isProxySet()) {
            // TODO: Finish proxy settings replacement

            String proxyHostName = realm.getProxyHostName();
            log.debug("Proxy host name: " + proxyHostName);
            if (proxyHostName != null) {
                url = new URL(url.getProtocol(), proxyHostName, url.getPort(), url.getFile());
            }

            int proxyPort = realm.getProxyPort();
            if (proxyPort >= 0) {
                url = new URL(url.getProtocol(), url.getHost(), proxyPort, url.getFile());
            } else {
                url = new URL(url.getProtocol(), url.getHost(), url.getDefaultPort(), url.getFile());
            }

            String proxyPrefix = realm.getProxyPrefix();
            if (proxyPrefix != null) {
                url = new URL(url.getProtocol(), url.getHost(), url.getPort(),
                        url.getFile().substring(proxyPrefix.length()));
            }
        } else {
            log.warn("No proxy set.");
        }

        return url.toString() + "?" + PW_RESET_ID + "=" + uuid;
    }

    /**
     * Get base path (collection path) where reset password requests will be saved permanently
     */
    private String getResetPasswordRequestsBasePath() throws Exception {
        String configuredBasePath = getResourceConfigProperty("change-password-requests-path");
        String basePath;
        if (configuredBasePath != null) {
            if (!configuredBasePath.startsWith("/")) {
                basePath = "/" + configuredBasePath;
            } else {
                basePath = configuredBasePath;
            }
        } else {
            String DEFAULT_BASE_PATH = "/reset-password-requests";
            log.warn("No base path configured. Will use default value: " + DEFAULT_BASE_PATH);
            basePath = DEFAULT_BASE_PATH;
        }
        return basePath;
    }

    /**
     * Get forgot password request UUID. Overwrite this method in case you have a different query string parameter for the UUID
     * @param request HTTP request containing the UUID
     */
    protected String getForgotPasswordRequestUUID(HttpServletRequest request) {
        return request.getParameter(PW_RESET_ID);
    }

    /**
     * Get from / sender email address for email being sent to invited user
     * @return from / sender email address and null when not configured
     */
    private String getFromEmail() throws Exception {
        String fromEmail = getResourceConfigProperty("smtpFrom");
        if (fromEmail != null) {
            return fromEmail;
        } else {
            fromEmail = getYanel().getAdministratorEmail();
            log.warn(
                    "From / sender email address not configured inside resource configuration, hence try to user administrator email '"
                            + fromEmail + "' of Yanel instance ...");
            return fromEmail;
        }
    }

    /**
     * Send email to user requesting to reset the password
     * @param guid UUID which is part of the change password link
     * @return UUID
     */
    protected String sendEmail(String guid, String emailAddress) throws Exception {
        String emailSubject = "Reset password request needs your confirmation";

        String emailBody = generateEmailBody(guid);

        String from = getFromEmail();
        String to = emailAddress;

        String emailServer = getResourceConfigProperty(SMTP_HOST_PROPERTY_NAME);
        if (emailServer != null) {
            int port = Integer.parseInt(getResourceConfigProperty(SMTP_PORT_PROPERTY_NAME));
            org.wyona.yanel.core.util.MailUtil.send(emailServer, port, from, to, emailSubject, emailBody);
        } else {
            org.wyona.yanel.core.util.MailUtil.send(from, to, emailSubject, emailBody);
        }
        return guid;
    }

    /**
     *
     */
    private String getPersistentRequestPath(String guid) throws Exception {
        return getResetPasswordRequestsBasePath() + "/" + guid + ".xml";
    }

    /**
     * Generate email body
     */
    private String generateEmailBody(String uuid) throws Exception {
        String emailBody = "Please go to the following URL to reset password: <" + getURL(uuid) + ">.";
        String hrsValid = getResourceConfigProperty(HOURS_VALID_PROPERTY_NAME);
        if (hrsValid == null) {
            hrsValid = "" + DEFAULT_TOTAL_VALID_HRS;
        }
        emailBody = emailBody + "\n\nNOTE: This link is only available during the next " + hrsValid + " hours!";
        if (log.isDebugEnabled())
            log.debug(emailBody);
        return emailBody;
    }
}