/******************************************************************************* * 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.cloudfoundry.identity.uaa.codestore.ExpiringCode; import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore; import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.error.UaaException; import org.cloudfoundry.identity.uaa.message.MessageService; import org.cloudfoundry.identity.uaa.message.MessageType; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; 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.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.NoSuchClientException; import org.springframework.util.StringUtils; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; import java.sql.Timestamp; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import static org.cloudfoundry.identity.uaa.codestore.ExpiringCodeType.EMAIL; import static org.cloudfoundry.identity.uaa.util.UaaUrlUtils.findMatchingRedirectUri; public class EmailChangeEmailService implements ChangeEmailService { private final TemplateEngine templateEngine; private final MessageService messageService; private final ScimUserProvisioning scimUserProvisioning; private final ExpiringCodeStore codeStore; private final ClientDetailsService clientDetailsService; private static final int EMAIL_CHANGE_LIFETIME = 30 * 60 * 1000; public static final String CHANGE_EMAIL_REDIRECT_URL = "change_email_redirect_url"; public EmailChangeEmailService(TemplateEngine templateEngine, MessageService messageService, ScimUserProvisioning scimUserProvisioning, ExpiringCodeStore codeStore, ClientDetailsService clientDetailsService) { this.templateEngine = templateEngine; this.messageService = messageService; this.scimUserProvisioning = scimUserProvisioning; this.codeStore = codeStore; this.clientDetailsService = clientDetailsService; } @Override public void beginEmailChange(String userId, String email, String newEmail, String clientId, String redirectUri) { ScimUser user = scimUserProvisioning.retrieve(userId); List<ScimUser> results = scimUserProvisioning.query("userName eq \"" + newEmail + "\" and origin eq \"" + OriginKeys.UAA + "\""); if (user.getUserName().equals(user.getPrimaryEmail())) { if (!results.isEmpty()) { throw new UaaException("Conflict", 409); } } String code = generateExpiringCode(userId, newEmail, clientId, redirectUri); String htmlContent = getEmailChangeEmailHtml(email, newEmail, code); if(htmlContent != null) { String subject = getSubjectText(); messageService.sendMessage(newEmail, MessageType.CHANGE_EMAIL, subject, htmlContent); } } private String generateExpiringCode(String userId, String newEmail, String clientId, String redirectUri) { Map<String, String> codeData = new HashMap<>(); codeData.put("user_id", userId); codeData.put("client_id", clientId); codeData.put("redirect_uri", redirectUri); codeData.put("email", newEmail); return codeStore.generateCode(JsonUtils.writeValueAsString(codeData), new Timestamp(System.currentTimeMillis() + EMAIL_CHANGE_LIFETIME), EMAIL.name()).getCode(); } @Override public Map<String, String> completeVerification(String code) { ExpiringCode expiringCode = codeStore.retrieveCode(code); if ((null == expiringCode) || ((null != expiringCode.getIntent()) && !EMAIL.name().equals(expiringCode.getIntent()))) { throw new UaaException("Error", 400); } Map<String, String> codeData = JsonUtils.readValue(expiringCode.getData(), new TypeReference<Map<String, String>>() {}); String userId = codeData.get("user_id"); String email = codeData.get("email"); ScimUser user = scimUserProvisioning.retrieve(userId); if (user.getUserName().equals(user.getPrimaryEmail())) { user.setUserName(email); } user.getEmails().clear(); user.setPrimaryEmail(email); scimUserProvisioning.update(userId, user); String clientId = codeData.get("client_id"); String redirectLocation = null; if (clientId != null) { String redirectUri = codeData.get("redirect_uri") == null ? "" : codeData.get("redirect_uri"); try { ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId); Set<String> redirectUris = clientDetails.getRegisteredRedirectUri() == null ? Collections.emptySet() : clientDetails.getRegisteredRedirectUri(); String changeEmailRedirectUrl = (String) clientDetails.getAdditionalInformation().get(CHANGE_EMAIL_REDIRECT_URL); redirectLocation = findMatchingRedirectUri(redirectUris, redirectUri, changeEmailRedirectUrl); } catch (NoSuchClientException nsce) {} } Map<String,String> result = new HashMap<>(); result.put("userId", user.getId()); result.put("username", user.getUserName()); result.put("email", user.getPrimaryEmail()); result.put("redirect_url", redirectLocation); return result; } private String getSubjectText() { if (IdentityZoneHolder.get().equals(IdentityZone.getUaa())) { String companyName = IdentityZoneHolder.resolveBranding().getCompanyName(); return StringUtils.hasText(companyName) ? companyName + " Email change verification" : "Account Email change verification"; } else { return IdentityZoneHolder.get().getName() + " Email change verification"; } } private String getEmailChangeEmailHtml(String email, String newEmail, String code) { String verifyUrl = UaaUrlUtils.getUaaUrl("/verify_email"); final Context ctx = new Context(); if (IdentityZoneHolder.get().equals(IdentityZone.getUaa())) { String companyName = IdentityZoneHolder.resolveBranding().getCompanyName(); ctx.setVariable("serviceName", StringUtils.hasText(companyName) ? companyName : "Cloud Foundry"); ctx.setVariable("servicePhrase", StringUtils.hasText(companyName) ? "a " + companyName + " account" : "an account"); } else { ctx.setVariable("serviceName", IdentityZoneHolder.get().getName()); ctx.setVariable("servicePhrase", IdentityZoneHolder.get().getName()); } ctx.setVariable("code", code); ctx.setVariable("newEmail", newEmail); ctx.setVariable("email", email); ctx.setVariable("verifyUrl", verifyUrl); return templateEngine.process("verify_email", ctx); } }