/******************************************************************************* * Cloud Foundry * Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved. * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). * You may not use this product except in compliance with the License. * * This product includes a number of subcomponents with * separate copyright notices and license terms. Your use of these * subcomponents is subject to the terms and conditions of the * subcomponent's license, as noted in the LICENSE file. *******************************************************************************/ package org.cloudfoundry.identity.uaa.account; import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cloudfoundry.identity.uaa.codestore.ExpiringCode; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.login.AccountSavingAuthenticationSuccessHandler; import org.cloudfoundry.identity.uaa.message.MessageService; import org.cloudfoundry.identity.uaa.message.MessageType; import org.cloudfoundry.identity.uaa.scim.endpoints.PasswordChange; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.util.UaaUrlUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.springframework.http.HttpStatus; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.sql.Timestamp; import java.util.Map; import java.util.regex.Pattern; import static org.springframework.util.StringUtils.hasText; @Controller public class ResetPasswordController { protected final Log logger = LogFactory.getLog(getClass()); private final ResetPasswordService resetPasswordService; private final MessageService messageService; private final TemplateEngine templateEngine; private final Pattern emailPattern; private final ExpiringCodeStore codeStore; private final UaaUserDatabase userDatabase; private final AccountSavingAuthenticationSuccessHandler successHandler; public ResetPasswordController(ResetPasswordService resetPasswordService, MessageService messageService, TemplateEngine templateEngine, ExpiringCodeStore codeStore, UaaUserDatabase userDatabase, AccountSavingAuthenticationSuccessHandler successHandler) { this.resetPasswordService = resetPasswordService; this.messageService = messageService; this.templateEngine = templateEngine; this.successHandler = successHandler; emailPattern = Pattern.compile("^\\S+@\\S+\\.\\S+$"); this.codeStore = codeStore; this.userDatabase = userDatabase; } @RequestMapping(value = "/forgot_password", method = RequestMethod.GET) public String forgotPasswordPage(Model model, @RequestParam(required = false, value = "client_id") String clientId, @RequestParam(required = false, value = "redirect_uri") String redirectUri, HttpServletResponse response) { if(!IdentityZoneHolder.get().getConfig().getLinks().getSelfService().isSelfServiceLinksEnabled()) { return handleSelfServiceDisabled(model, response, "error_message_code", "self_service_disabled"); } model.addAttribute("client_id", clientId); model.addAttribute("redirect_uri", redirectUri); return "forgot_password"; } @RequestMapping(value = "/forgot_password.do", method = RequestMethod.POST) public String forgotPassword(Model model, @RequestParam("email") String email, @RequestParam(value = "client_id", defaultValue = "") String clientId, @RequestParam(value = "redirect_uri", defaultValue = "") String redirectUri, HttpServletResponse response) { if(!IdentityZoneHolder.get().getConfig().getLinks().getSelfService().isSelfServiceLinksEnabled()) { return handleSelfServiceDisabled(model, response, "error_message_code", "self_service_disabled"); } if (emailPattern.matcher(email).matches()) { forgotPassword(email, clientId, redirectUri); return "redirect:email_sent?code=reset_password"; } else { return handleUnprocessableEntity(model, response, "message_code", "form_error"); } } private void forgotPassword(String email, String clientId, String redirectUri) { String subject = getSubjectText(); String htmlContent = null; String userId = null; try { ForgotPasswordInfo forgotPasswordInfo = resetPasswordService.forgotPassword(email, clientId, redirectUri); userId = forgotPasswordInfo.getUserId(); htmlContent = getCodeSentEmailHtml(forgotPasswordInfo.getResetPasswordCode().getCode()); } catch (ConflictException e) { htmlContent = getResetUnavailableEmailHtml(email); userId = e.getUserId(); } catch (NotFoundException e) { logger.error("User with email address " + email + " not found."); } if (htmlContent != null && userId != null) { messageService.sendMessage(email, MessageType.PASSWORD_RESET, subject, htmlContent); } } private String getSubjectText() { String serviceName = getServiceName(); if (StringUtils.isEmpty(serviceName)) { return "Account password reset request"; } return serviceName + " account password reset request"; } private String getCodeSentEmailHtml(String code) { String resetUrl = UaaUrlUtils.getUaaUrl("/reset_password"); final Context ctx = new Context(); ctx.setVariable("serviceName", getServiceName()); ctx.setVariable("code", code); ctx.setVariable("resetUrl", resetUrl); return templateEngine.process("reset_password", ctx); } private String getResetUnavailableEmailHtml(String email) { String hostname = UaaUrlUtils.getUaaHost(); final Context ctx = new Context(); ctx.setVariable("serviceName", getServiceName()); ctx.setVariable("email", email); ctx.setVariable("hostname", hostname); return templateEngine.process("reset_password_unavailable", ctx); } private String getServiceName() { if (IdentityZoneHolder.get().equals(IdentityZone.getUaa())) { String companyName = IdentityZoneHolder.resolveBranding().getCompanyName(); return StringUtils.hasText(companyName) ? companyName : "Cloud Foundry"; } else { return IdentityZoneHolder.get().getName(); } } @RequestMapping(value = "/email_sent", method = RequestMethod.GET) public String emailSentPage(@ModelAttribute("code") String code) { return "email_sent"; } @RequestMapping(value = "/reset_password", method = RequestMethod.GET, params = { "code" }) public String resetPasswordPage(Model model, HttpServletResponse response, @RequestParam("code") String code) { ExpiringCode expiringCode = checkIfUserExists(codeStore.retrieveCode(code)); if (expiringCode==null) { return handleUnprocessableEntity(model, response, "message_code", "bad_code"); } else { PasswordChange passwordChange = JsonUtils.readValue(expiringCode.getData(), PasswordChange.class); String userId = passwordChange.getUserId(); UaaUser uaaUser = userDatabase.retrieveUserById(userId); String newCode = codeStore.generateCode(expiringCode.getData(), new Timestamp(System.currentTimeMillis() + (10 * 60 * 1000)), expiringCode.getIntent()).getCode(); model.addAttribute("code", newCode); model.addAttribute("email", uaaUser.getEmail()); return "reset_password"; } } public ExpiringCode checkIfUserExists(ExpiringCode code) { if (code==null) { logger.debug("reset_password ExpiringCode object is null. Aborting."); return null; } if (!hasText(code.getData())) { logger.debug("reset_password ExpiringCode["+code.getCode()+"] data string is null or empty. Aborting."); return null; } Map<String,String> data = JsonUtils.readValue(code.getData(), new TypeReference<Map<String,String>>() {}); if (!hasText(data.get("user_id"))) { logger.debug("reset_password ExpiringCode["+code.getCode()+"] user_id string is null or empty. Aborting."); return null; } String userId = data.get("user_id"); try { userDatabase.retrieveUserById(userId); } catch (UsernameNotFoundException e) { logger.debug("reset_password ExpiringCode["+code.getCode()+"] user_id is invalid. Aborting."); return null; } return code; } @RequestMapping(value = "/reset_password.do", method = RequestMethod.POST) public void resetPassword(Model model, @RequestParam("code") String code, @RequestParam("email") String email, @RequestParam("password") String password, @RequestParam("password_confirmation") String passwordConfirmation, HttpServletRequest request, HttpServletResponse response, HttpSession session) { } private String handleUnprocessableEntity(Model model, HttpServletResponse response, String attributeKey, String attributeValue) { model.addAttribute(attributeKey, attributeValue); response.setStatus(HttpStatus.UNPROCESSABLE_ENTITY.value()); return "forgot_password"; } private String handleSelfServiceDisabled(Model model, HttpServletResponse response, String attributeKey, String attributeValue) { model.addAttribute(attributeKey, attributeValue); response.setStatus(HttpStatus.NOT_FOUND.value()); return "error"; } }