All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.techempower.gemini.pyxis.handler.PasswordResetHandler Maven / Gradle / Ivy

There is a newer version: 3.3.14
Show newest version
/*******************************************************************************
 * Copyright (c) 2018, TechEmpower, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name TechEmpower, Inc. nor the names of its
 *       contributors may be used to endorse or promote products derived from
 *       this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL TECHEMPOWER, INC. BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/

package com.techempower.gemini.pyxis.handler;

import java.util.*;

import com.techempower.gemini.*;
import com.techempower.gemini.context.*;
import com.techempower.gemini.email.*;
import com.techempower.gemini.email.outbound.*;
import com.techempower.gemini.input.*;
import com.techempower.gemini.input.processor.*;
import com.techempower.gemini.input.validator.*;
import com.techempower.gemini.path.*;
import com.techempower.gemini.path.annotation.*;
import com.techempower.gemini.pyxis.*;
import com.techempower.gemini.pyxis.password.*;
import com.techempower.helper.*;
import com.techempower.log.*;
import com.techempower.util.*;

/**
 * Provides conventional password-reset functionality for Gemini/Pyxis web
 * applications.  The basic user experience flow is as follows:
 *   
    *
  1. STEP 0: User navigates a link labeled "I've forgotten my password." *
  2. STEP 1: The resulting page prompts the user for their username. *
  3. STEP 2: Assuming a matching user is found, a temporary authorization * ticket is generated and e-mailed to the user's e-mail account on file; * a confirmation page is rendered. *
  4. STEP 3: In the generated e-mail, a URL will be rendered (optionally, * the ticket alone can be rendered). *
  5. STEP 4: When the user navigates to that URL, a second page will allow * the user to provide a new password. *
  6. STEP 5: After providing a new password, the user is directed to a final * "complete" page. The user is not automatically logged in. *
* The e-mail template ID is E-PasswordResetAuthorization *

* The following mustache templates are used: *

    *
  1. auth/password-reset-request.mustache - For STEP 1 above. *
  2. auth/password-reset-request-confirmed.mustache - For STEP 2 above. *
  3. auth/password-reset-process.mustache - For STEP 4 above. *
  4. auth/password-reset-complete.mustache - For STEP 5 above. *
* This functionality requires that your User class is based on BasicWebUser * and includes the following fields: *
    *
  1. PasswordResetTicket - 15 alphanumeric characters *
  2. PasswordResetExpiration - Date *
* Configuration properties: *
    *
  • PasswordReset.FromAddress - the email address from which to send * the authorization ticket email.
  • *
  • PasswordReset.ExpirationDays - How many days should a new password- * reset ticket remain valid? Default is 5.
  • *
  • PasswordReset.TemplateRelativePath - What is the relative path to the * Mustache templates? By default the relative path is "auth/".
  • *
*/ public class PasswordResetHandler extends MethodSegmentHandler implements Configurable { // // Constants. // public static final String COMPONENT_CODE = "hPsR"; public static final String DEFAULT_TEMPLATE_PATH = "/auth/"; public static final int DEFAULT_EXPIRATION_DAYS = 5; public static final String TEMPLATE_RESET_REQUEST = "password-reset-request"; public static final String TEMPLATE_RESET_CONFIRMED = "password-reset-request-confirmed"; public static final String TEMPLATE_RESET = "password-reset-process"; public static final String TEMPLATE_RESET_COMPLETE = "password-reset-complete"; public static final String TEMPLATE_RESET_NOT_FOUND = "password-reset-not-found"; public static final String EMAIL_TEMPLATE_NAME = "E-PasswordResetAuthorization"; // // Member variables. // private final PyxisSecurity security; private String fromAddress = ""; private int expirationDays = DEFAULT_EXPIRATION_DAYS; // // Member methods. // /** * Constructor. */ public PasswordResetHandler(GeminiApplication application) { super(application, COMPONENT_CODE); this.security = application.getSecurity(); // Ask the EmailTemplater to load our template when it configures. EmailTemplater templater = application.getEmailTemplater(); templater.addTemplateToLoad(getEmailTemplateName()); application.getConfigurator().addConfigurable(this); } @Override public void configure(EnhancedProperties props) { EnhancedProperties.Focus focus = props.focus("PasswordReset."); this.fromAddress = focus.get("FromAddress", this.fromAddress); this.expirationDays = focus.getInt("ExpirationDays", DEFAULT_EXPIRATION_DAYS); setBaseTemplatePath(focus.get("TemplateRelativePath", DEFAULT_TEMPLATE_PATH)); } /** * Gets the reset-request validation rules. */ protected ValidatorSet getResetRequestValidatorSet() { return standardResetRequestValidatorSet; } /** * Hard-coded default reset-request validation rules. */ private final ValidatorSet standardResetRequestValidatorSet = new ValidatorSet( new Lowercase("un"), new LengthValidator("un", BasicUser.USERNAME_LENGTH, false) .message("Please provide a valid username.") ); /** * Gets the password-reset validation rules. */ protected ValidatorSet getPasswordResetValidatorSet() { return new ValidatorSet( new RequiredValidator("newpw") .message("A new password is required."), new RepeatValidator("newpw", "confirmpw") .message("New password and confirmation do not match."), new ShortCircuitValidator.Wrapper( new PasswordComplexityValidator("newpw", security)) ); } /** * STEP 1: Handles a request for a new password reset ticket to be generated * and sent to the user's email address. This will display a confirmation * page to the user. Templates used: *
    *
  • auth/password-reset-request.mustache: In this page, a form will * prompt the user to identify themselves via username. The form * elements on the page are "un" (username field) and "submit" (submit * button).
  • *
  • auth/password-reset-request-confirmed.mustache: Announces that an * e-mail has been sent out.
  • *
*/ @PathDefault @Get public boolean getResetRequest(Context context) { template(TEMPLATE_RESET_REQUEST); return render(); } /** * Handles the form submission from STEP 1. */ @PathDefault @Post public boolean resetRequest(Context context) { template(TEMPLATE_RESET_REQUEST); // Check for submission. final Input input = getResetRequestValidatorSet().process(context); if (input.passed()) { // Send the e-mail and notify the user that we've done so. final Query values = input.values(); final String username = values.get("un"); final BasicWebUser user = (BasicWebUser)security.findUser(username); // Did we find the user? if (user != null) { // Update the user. final String ticket = user.generateNewPasswordResetTicket(this.expirationDays); saveUser(user); // Send the email. sendAuthorizationEmail(context, user, ticket); return handleResetRequestSuccess(); } else { return handleResetRequestInvalid(); } } return validationFailure(input); } /** * Handle a successful reset request. */ protected boolean handleResetRequestSuccess() { template(TEMPLATE_RESET_CONFIRMED); delivery().status("ticket-mailed"); return message("A password reset ticket has been e-mailed."); } /** * Handle an invalid username reset request. */ protected boolean handleResetRequestInvalid() { delivery().message("User not found."); return badRequest("invalid"); } /** * Sends the authorization e-mail to the user. Macros: *
    *
  • $UN = Username
  • *
  • $FN = First name
  • *
  • $LN = Last name
  • *
  • $EM = Email address
  • *
  • $VT = Authorization ticket
  • *
  • $ED = Number of days before the ticket expires
  • *
  • $URL = Full authorized URL
  • *
* The default email template name is "E-PasswordResetAuthorization". */ protected void sendAuthorizationEmail(Context context, BasicWebUser user, String ticket) { final EmailTemplater templater = app().getEmailTemplater(); final Map macros = new HashMap<>(10); macros.put("$UN", user.getUserUsername()); macros.put("$UUN", NetworkHelper.encodeUrl(user.getUserUsername())); macros.put("$FN", user.getUserFirstname()); macros.put("$LN", user.getUserLastname()); macros.put("$EM", user.getUserEmail()); macros.put("$VT", ticket); macros.put("$SD", app().getInfrastructure().getStandardDomain()); macros.put("$SSD", app().getInfrastructure().getSecureDomain()); macros.put("$ED", "" + this.expirationDays); // Construct the URL. macros.put("$URL", getAuthorizationUrl(user, ticket)); // Get a suitable author address. final String authorAddress = this.getFromAddress(); // Send the mail. final EmailPackage email = templater.process(getEmailTemplateName(), macros, authorAddress, user.getUserEmail()); if (email != null) { app().getEmailServicer().sendMail(email); } else { l("Email could not be fetched from EmailTemplater."); } } /** * Returns a suitable email address to use in an email's "from" field. * @return the fromAddress */ protected String getFromAddress() { if (StringHelper.isEmpty(this.fromAddress)) { l("Using administrator e-mail address for sending password-reset email: " + app().getAdministratorEmail(), LogLevel.MINIMUM); return app().getAdministratorEmail(); } else { return this.fromAddress; } } /** * Generates an authorization URL that will be sent to the user by the * method sendAuthorizationEmail. This can be overloaded to return a URL * that is suitable for any URL re-writing rules in place. */ protected String getAuthorizationUrl(BasicWebUser user, String ticket) { final StringBuilder builder = new StringBuilder(500); builder.append(app().getInfrastructure().getSecureUrl()); if (app().getInfrastructure().getSecureUrl().endsWith("/")) { builder.append(getBaseUri().substring(1)); // Omit leading / } else { builder.append(getBaseUri()); } builder.append("/auth?un=") .append(NetworkHelper.encodeUrl(user.getUserUsername())); builder.append("&vt=") .append(ticket); return builder.toString(); } /** * Saves a user to the database. Overload to implement any special cache * maintenance that may be necessary (such as notifying peer applications). */ protected void saveUser(BasicWebUser user) { store().put(user); } /** * STEP 4: Handles an authorized request to change password. Templates * used: *
    *
  • auth/password-reset-process.mustache: Displays a password-reset * form with elements "pw" (a FormPasswordField configured to require * confirmation) and "submit" (a FormSubmitButton).
  • *
  • auth/password-reset-complete.mustache: The password-reset process * is complete.
  • *
*/ @PathSegment("auth") @Post @Get public boolean authorize(Context context) { final String username = query().get("un", ""); final String ticket = query().get("vt", ""); template(TEMPLATE_RESET_NOT_FOUND); // Get a reference to the user. final BasicWebUser user = (BasicWebUser)this.security.findUser(username); if (user != null) { // Is the authorization ticket correct and not expired? if (user.isPasswordResetAuthorized(ticket)) { template(TEMPLATE_RESET); // Check for submission. if (context.isPost()) { final Input input = getPasswordResetValidatorSet().process(context); if (input.passed()) { // Change the password. final PasswordProposal proposal = new PasswordProposal( input.values().get("newpw"), user.getUserUsername(), user, context); security.passwordChange(proposal); saveUser(user); // Go to the success/complete page. template(TEMPLATE_RESET_COMPLETE); return message("Password change complete."); } else { return validationFailure(input); } } return render(); } } delivery().message("Invalid password-reset ticket."); return badRequest("invalid-ticket"); } /** * Overload if desired to return a different email template name. */ protected String getEmailTemplateName() { return EMAIL_TEMPLATE_NAME; } } // End PasswordResetHandler.




© 2015 - 2024 Weber Informatics LLC | Privacy Policy