Java tutorial
/** * Copyright 2014 University of Chicago * * 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.apache.org/licenses/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. * * Author: Daniel Yu <danielyu@uchicago.edu> */ package edu.uchicago.duo.web; import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; import edu.uchicago.duo.domain.DuoAllIntegrationKeys; import edu.uchicago.duo.domain.DuoPersonObj; import edu.uchicago.duo.service.DuoObjInterface; import edu.uchicago.duo.validator.DeviceExistDuoValidator; import static edu.uchicago.duo.web.DuoEnrollController.sortByValue; import java.io.UnsupportedEncodingException; import java.security.Principal; import java.sql.Date; import java.text.Collator; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.Locale; import java.util.TreeMap; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.validation.Valid; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.log4j.Logger; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.propertyeditors.CustomDateEditor; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.SmartValidator; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.bind.support.SessionStatus; @Controller @RequestMapping("/secure/enrollment") @SessionAttributes("DuoPerson") public class DuoEnrollController { //get log4j handler protected final Log logger = LogFactory.getLog(getClass()); /// @Autowired private DuoObjInterface duoPhoneService; /// @Autowired private DuoObjInterface duoUsrService; /// @Autowired private DuoObjInterface duoTokenService; /// @Autowired private DeviceExistDuoValidator deviceExistDuoValidator; /// @Autowired SmartValidator validator; /** * ********************************************************** * * Private Methods Below * *********************************************************** */ private String getIPForLog(HttpServletRequest request) { String sourceIPAddr = request.getRemoteAddr(); if (sourceIPAddr == null || sourceIPAddr.startsWith("127.")) { sourceIPAddr = request.getHeader("x-forwarded-for"); } sourceIPAddr = "[" + sourceIPAddr + "]"; return sourceIPAddr; } /** * ********************************************************** * * Spring Controller Methods Below * *********************************************************** */ @InitBinder public void initBinder(WebDataBinder binder) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true)); } /* * Below initalize the model and return the view, Setup NEW session Attribute for "DuoPerson", * according to Spring Docs, it is recommended to NOT share session attributes among different controller. * It makes sense to me in that when user bookmarked this specific entry point, sharing a common session attribute may not * get all the value the controller needed. * Note about session attribute: * @ModelAttribute("DuoPerson") DuoPersonObj duoperson <Look for existing session attribute> * @ModelAttribute DuoPersonObj duoperson <Initalize a New session attribute> */ @RequestMapping(method = RequestMethod.GET) public String initForm(HttpServletRequest request, Principal principal, ModelMap model, @ModelAttribute DuoPersonObj duoperson, HttpSession session, SessionStatus status) throws UnsupportedEncodingException, JSONException, Exception { //Below getting SSO Attributes for Shibboleth Support(UChicago) // duoperson.setUsername(principal.getName()); // duoperson.setFullName(request.getHeader("givenName")+ " " + request.getHeader("sn")); // duoperson.setEmail(request.getHeader("mail")); // duoperson.setChicagoID(request.getHeader("chicagoID")); //Below setting Static Attributes for Local Testing duoperson.setUsername("DuoTestUser"); duoperson.setFullName("DUO Testuser"); duoperson.setEmail("testuser@duotest.com"); logger.info("2FA Info - " + getIPForLog(request) + " - " + "Username:" + duoperson.getUsername() + "|SID:" + request.getSession().getId()); //Setup Default Selection Values for the wizard form duoperson.setChoosenDevice("mobile"); duoperson.setDeviceOS("apple ios"); duoperson.setCountryDialCode("+1"); //Initalize Model with some variables and push that into SessionAttribute model.addAttribute("DuoPerson", duoperson); //return form view return processPage(2, duoperson, request, principal, null, session, status, model); } @RequestMapping(method = RequestMethod.POST, params = "reset") public String resetform(ModelMap model, @ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpSession session, SessionStatus status) { status.setComplete(); return "redirect:/secure"; } @RequestMapping(method = RequestMethod.POST, params = "back") public String goBack(@RequestParam("_backpage") final int backPage, ModelMap model, @ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpSession session, SessionStatus status) { if (backPage == 31) { return "DuoEnrollTablet"; } return "DuoEnrollStep" + backPage; } @RequestMapping(method = RequestMethod.POST, params = "enrollsteps") public String processPage(@RequestParam("_page") final int nextPage, @ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpServletRequest request, Principal principal, BindingResult result, HttpSession session, SessionStatus status, ModelMap model) throws UnsupportedEncodingException, JSONException, Exception { //Redirect for Enroll Another Device if (nextPage == 0) { status.setComplete(); return "redirect:/secure/enrollment"; } if (nextPage == 2) { String userId = duoUsrService.getObjByParam(duoperson.getUsername(), null, "userId"); duoperson.setUser_id(userId); if (userId != null) { //Session Attribute "Duo User ID" - UPDATED (Depends) session.setAttribute("duoUserId", userId); model.put("existingUser", true); } } //Redirect Depends on the type of Device that is being enroll if (nextPage == 3) { switch (duoperson.getChoosenDevice()) { case "tablet": duoperson.setDeviceOS(null); return "DuoEnrollTablet"; case "token": return "DuoEnrollToken"; } } if (nextPage == 31) { validator.validate(duoperson, result, DuoPersonObj.TabletInfoValidation.class); if (result.hasErrors()) { return "DuoEnrollTablet"; } return "DuoEnrollStep5"; } if (nextPage == 32) { validator.validate(duoperson, result, DuoPersonObj.TokenInfoValidation.class); if (result.hasErrors()) { return "DuoEnrollToken"; } return processEnroll(request, duoperson, result, session, status, model); } //Validation on Submission of Phone Number, to make sure Phone Number has not been registered or belong to someone else if (nextPage == 4) { validator.validate(duoperson, result, DuoPersonObj.PhoneNumberValidation.class); if (result.hasErrors()) { return "DuoEnrollStep3"; } deviceExistDuoValidator.validate(duoperson, result); if (result.hasErrors()) { return "DuoEnrollStep3"; } if (duoperson.getChoosenDevice().equals("landline")) { return "DuoPhoneVerify"; } } if (nextPage == 5) { if (duoperson.getDeviceOS().equals("unknown")) { return "DuoPhoneVerify"; } } //Check on Activation Status on DUO Mobile App, don't let user move on until confirm the device has been activated if (nextPage == 6) { String activeStatus; activeStatus = duoPhoneService.getObjStatusById(duoperson.getPhone_id()); if (activeStatus.equals("false")) { if (duoperson.getQRcode() == null) { String qrCode = duoPhoneService.objActionById(duoperson.getPhone_id(), "qrCode"); duoperson.setQRcode(qrCode); } logger.error("2FA Error - " + getIPForLog(request) + " - " + duoperson.getUsername() + "|DeviceID: " + duoperson.getPhone_id() + " NOT ACTIVATED"); model.put("deviceNotActive", true); return "DuoActivationQR"; } model.put("deviceActive", "Yes"); logger.info("2FA Info - " + getIPForLog(request) + " - " + duoperson.getUsername() + " Successfully Registered: PhoneNumber:" + duoperson.getPhonenumber() + " Tablet Name:" + duoperson.getTabletName()); return "DuoEnrollSuccess"; } //Traverse Multipage Form, DuoEnrollStep*.jsp return "DuoEnrollStep" + nextPage; } @RequestMapping(value = "/phoneverify.json/{action}", method = RequestMethod.GET) @ResponseBody public String callToVerify(@ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpSession session, @PathVariable String action) { String callInfo = null; String callState = null; Map<String, Object> verifyInfo = new HashMap<>(); switch (action) { case "call": verifyInfo = duoPhoneService.verifyObj(duoperson.getCompletePhonenumber(), duoperson.getLandLineExtension(), action); duoperson.setPhoneVerifyPin(verifyInfo.get("pin").toString()); duoperson.setPhoneVerifyTxid(verifyInfo.get("txid").toString()); return "CALLING"; case "status": verifyInfo = duoPhoneService.verifyObj(duoperson.getPhoneVerifyTxid(), null, action); callInfo = verifyInfo.get("info").toString(); callState = verifyInfo.get("state").toString(); break; } return callInfo; } @RequestMapping(value = "/phoneverify.json/verify/{inputpin}", method = RequestMethod.GET) @ResponseBody public String verifyPin(@ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpSession session, @PathVariable String inputpin, HttpServletRequest request) { String correctPin = null; correctPin = duoperson.getPhoneVerifyPin(); if (inputpin.equals(correctPin)) { duoperson.setPhoneOwnerVerified(true); logger.info("2FA Info - " + getIPForLog(request) + " - " + "Username:" + duoperson.getUsername() + " VERIFIED PhoneNumber:" + duoperson.getPhonenumber() + " Extension:" + duoperson.getLandLineExtension()); return "VERIFIED"; } else { duoperson.setPhoneOwnerVerified(false); return "INCORRECT"; } } @RequestMapping("/activationstatus.json") @ResponseBody public String reportStatus(@ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpSession session) { return duoPhoneService.getObjStatusById(duoperson.getPhone_id()); } @RequestMapping(method = RequestMethod.POST, params = "sendsms") public String duoSendSMS(@ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpServletRequest request, BindingResult result, SessionStatus status, ModelMap model) throws UnsupportedEncodingException, Exception { duoPhoneService.objActionById(duoperson.getPhone_id(), "activationSMS"); logger.info("2FA Info - " + getIPForLog(request) + " - " + duoperson.getUsername() + " is sending ACTIVATION SMS to " + duoperson.getPhonenumber()); duoperson.setQRcode(null); return "DuoActivationQR"; } @RequestMapping(method = RequestMethod.POST, params = "genQRcode") public String genQRCode(@ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpServletRequest request, BindingResult result, SessionStatus status, ModelMap model) throws UnsupportedEncodingException, Exception { String qrCode = duoPhoneService.objActionById(duoperson.getPhone_id(), "qrCode"); duoperson.setQRcode(qrCode); logger.debug("2FA Debug - " + getIPForLog(request) + " - " + duoperson.getUsername() + "|DeviceID: " + duoperson.getPhone_id() + " QR code regenrated"); return "DuoActivationQR"; } @RequestMapping(value = "/2FAoptin") public String twoFactorOptIn( @RequestParam(value = "_action", required = false, defaultValue = "landed") final String action, @ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpSession session, HttpServletRequest request) { String result = null; switch (action) { case "landed": duoperson.setOptInStatus(true); break; case "optin": if (duoperson.isOptInStatus()) { result = duoUsrService.objActionById(duoperson.getChicagoID(), "AddUserToDuoForce"); if (result.equals("Y")) { duoperson.setOptInStatus(true); logger.info("2FA Info - " + getIPForLog(request) + " - " + duoperson.getUsername() + " has OPT-IN DuoForce Grouper Group"); return "DuoEnroll2FAOptResult"; } else { duoperson.setOptInStatus(false); return "DuoEnroll2FAOptResult"; //May be Routing to an Error page instead!!! } } else { return "redirect:/secure"; } } return "DuoEnroll2FAOptIn"; } @RequestMapping(value = "/2FAoptout") public String twoFactorOptOut( @RequestParam(value = "_action", required = false, defaultValue = "landed") final String action, @ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpSession session, HttpServletRequest request) { String result = null; switch (action) { case "landed": duoperson.setOptInStatus(true); break; case "optout": if (duoperson.isOptInStatus()) { return "redirect:/secure"; } else { result = duoUsrService.objActionById(duoperson.getChicagoID(), "RemoveUserFromDuoForce"); if (result.equals("Y")) { duoperson.setOptInStatus(false); logger.info("2FA Info - " + getIPForLog(request) + " - " + duoperson.getUsername() + " has OPT-OUT DuoForce Grouper Group"); return "DuoEnroll2FAOptResult"; } else { duoperson.setOptInStatus(true); return "DuoEnroll2FAOptResult"; //May be Routing to an Error page instead!!! } } } return "DuoEnroll2FAOptOut"; } /** * ********************************************************************* * Calling by DuoDeviceMgmt Controller, for Device Reactivation Purposes * ********************************************************************** */ @RequestMapping(value = "/deviceReactivation") public String deviceReactivation(@ModelAttribute("DuoPerson") DuoPersonObj duoperson, HttpServletRequest request, Principal principal, BindingResult result, HttpSession session, SessionStatus status, ModelMap model) throws UnsupportedEncodingException, JSONException, Exception { logger.info("2FA Info - " + getIPForLog(request) + " - " + "REACTIVATION Process-Device Data:" + duoperson.getChoosenDevice() + ' ' + duoperson.getPhonenumber() + ' ' + duoperson.getDeviceOS()); return "DuoEnrollStep5"; } /** * ************************************************************************ * Main Method to Handle the actual Device ID Creation and Association * between the Device and User * ************************************************************************* */ @RequestMapping(method = RequestMethod.POST, params = "enrollUserNPhone") public String processEnroll(HttpServletRequest request, @ModelAttribute("DuoPerson") DuoPersonObj duoperson, // @RequestParam(value = "_unknownMobileVerified", required = false, defaultValue = "N") final String unknownMobileVerified, BindingResult result, HttpSession session, SessionStatus status, ModelMap model) throws UnsupportedEncodingException, Exception { String phoneId; String tokenId; String userId; String qrCode; /** * ******************************************************************************************* * Enrollment Procedure, first thing first! * * Check Whether User is a registered DUO User and Register First-Time * User into DUO Database * ******************************************************************************************** */ if (duoperson.getUser_id() == null) { userId = duoUsrService.createObjByParam(duoperson.getUsername(), duoperson.getFullName(), duoperson.getEmail(), null, null); duoperson.setUser_id(userId); //Session Attribute "Duo User ID" - ADDED session.setAttribute("duoUserId", userId); logger.info("2FA Info - " + getIPForLog(request) + " - " + "Duo User Account created for: " + duoperson.getUsername() + "|DuoUserID:" + duoperson.getUser_id()); } //Attempts to try to Catpure F5/Browser Refresh during Device Activation, Not letting Users jump out without Activation success if (StringUtils.hasLength(duoperson.getPhone_id())) { String activeStatus = duoPhoneService.getObjStatusById(duoperson.getPhone_id()); if (activeStatus.equals("false")) { qrCode = duoPhoneService.objActionById(duoperson.getPhone_id(), "qrCode"); duoperson.setQRcode(qrCode); model.put("deviceNotActive", true); return "DuoActivationQR"; } else { logger.info("2FA Info - " + getIPForLog(request) + " - " + duoperson.getUsername() + " Successfully Registered: PhoneNumber:" + duoperson.getPhonenumber() + "Tablet Name:" + duoperson.getTabletName()); return "DuoEnrollSuccess"; } } /** * *********************************************************************************** * Enrollment Procedure for Type == unknown * * Unknown == Old CellPhone that doesn't support Duo Mobile App, but * still support SMS * *********************************************************************************** */ if (duoperson.getChoosenDevice().matches("mobile") && duoperson.getDeviceOS().matches("unknown")) { phoneId = duoPhoneService.createObjByParam(duoperson.getCompletePhonenumber(), duoperson.getChoosenDevice(), duoperson.getDeviceOS(), null, null); duoperson.setPhone_id(phoneId); duoPhoneService.associateObjs(duoperson.getUser_id(), phoneId); } /** * ******************************************************************** * Enrollment Procedure for Type == Mobile | Tablet * * 1st) Create the Phone/Tablet Device first in DUO DB * * 2nd) Link the newly create Phone/Tablet to the user * * 3rd) Generate and Display the Activation QR code for DUO Mobile App * Registration * ******************************************************************** */ if (duoperson.getChoosenDevice().matches("mobile|tablet") && !duoperson.getDeviceOS().matches("unknown")) { phoneId = duoPhoneService.createObjByParam(duoperson.getCompletePhonenumber(), duoperson.getChoosenDevice(), duoperson.getDeviceOS(), duoperson.getTabletName(), null); duoperson.setPhone_id(phoneId); duoPhoneService.associateObjs(duoperson.getUser_id(), phoneId); qrCode = duoPhoneService.objActionById(duoperson.getPhone_id(), "qrCode"); duoperson.setQRcode(qrCode); return "DuoActivationQR"; } /** * ****************************************** * Enrollment Procedure for Type == LandLine * ****************************************** */ if (duoperson.getChoosenDevice().matches("landline")) { phoneId = duoPhoneService.createObjByParam(duoperson.getCompletePhonenumber(), duoperson.getChoosenDevice(), null, null, duoperson.getLandLineExtension()); duoperson.setPhone_id(phoneId); duoPhoneService.associateObjs(duoperson.getUser_id(), phoneId); } /** * **************************************************************** * Enrollment Procedure for Type == Token * * 1) Validate against the database to see whether Token has been * register by somebody else || Token Existence in DB * * 2) Then ASSOCIATE the Token with the User * **************************************************************** */ if (duoperson.getChoosenDevice().matches("token")) { logger.debug("2FA Debug - " + getIPForLog(request) + "|" + duoperson.getUsername() + " is registering Token:" + duoperson.getTokenType() + "/" + duoperson.getTokenSerial()); deviceExistDuoValidator.validate(duoperson, result); if (result.hasErrors()) { return "DuoEnrollToken"; } tokenId = duoTokenService.getObjByParam(duoperson.getTokenSerial(), duoperson.getTokenType(), "tokenId"); duoperson.setTokenId(tokenId); duoTokenService.associateObjs(duoperson.getUser_id(), tokenId); String readabletype = null; switch (duoperson.getTokenType()) { case "d1": readabletype = "Duo-D100"; break; case "yk": readabletype = "YubiKey AES"; break; case "h6": readabletype = "HOTP-6"; break; case "h8": readabletype = "HOTP-8"; break; case "t6": readabletype = "TOTP-6"; break; case "t8": readabletype = "TOTP-8"; break; } duoperson.setTokenType(readabletype); } model.put("deviceActive", "Yes"); logger.info("2FA Info - " + getIPForLog(request) + " - " + duoperson.getUsername() + " Successfully Registered: PhoneNumber:" + duoperson.getPhonenumber() + " Tablet Name:" + duoperson.getTabletName() + " Token SN:" + duoperson.getTokenSerial()); return "DuoEnrollSuccess"; } /** * **************************************** * Drop Down List for Token Type Selection * * Used in: DuoEnrollToken.jsp * ***************************************** */ @ModelAttribute("tokenTypeList") public Map<String, String> populateTokenTypeList() { Map<String, String> tokenType = new LinkedHashMap<>(); tokenType.put("d1", "Duo-D100 hardware token"); tokenType.put("yk", "YubiKey AES hardware token"); tokenType.put("h6", "HOTP-6 hardware token"); tokenType.put("h8", "HOTP-8 hardware token"); tokenType.put("t6", "TOTP-6 hardware token"); tokenType.put("t8", "TOTP-8 hardware token"); return tokenType; } /** * **************************************** * Drop Down List for Tablet OS Selection * * Used in: DuoEnrollTablet.jsp * ****************************************** */ @ModelAttribute("tabletOSList") public Map<String, String> populateTabletOSList() { Map<String, String> tabletOS = new LinkedHashMap<>(); tabletOS.put("apple ios", "Apple IOS"); tabletOS.put("google android", "Google Android"); // tabletOS.put("windows phone", "Microsoft Windows for Surface"); return tabletOS; } /** * ********************************************************************* * Below are ALL FOR creating the International Dial Code Drop Down list * * Used in: DuoEnrollStep3.jsp * ********************************************************************* */ @ModelAttribute("countryDialList") public Map<String, String> populatecountryDialList() { List<DuoEnrollController.Country> countries = new ArrayList<>(); Map<String, String> dialCodes = new LinkedHashMap<>(); Map<String, String> sortedDialCodes = new LinkedHashMap<>(); PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); // // Get ISO countries, create Country object and // store in the collection. // String[] isoCountries = Locale.getISOCountries(); for (String country : isoCountries) { Locale locale = new Locale("en", country); String code = locale.getCountry(); String name = locale.getDisplayCountry(); if (!"".equals(code) && !"".equals(name)) { try { int dialCode = phoneUtil.parse("1112223333", code).getCountryCode(); countries.add(new DuoEnrollController.Country(code, name, dialCode)); } catch (Exception e) { } } } Collections.sort(countries, new DuoEnrollController.CountryComparator()); for (DuoEnrollController.Country country : countries) { dialCodes.put("+" + String.valueOf(country.dialCode), country.name); //dialCodes.put("+"+String.valueOf(country.code), country.name); } sortedDialCodes = sortByValue(dialCodes); return sortedDialCodes; } private static class Country { private String code; private String name; private int dialCode; Country(String code, String name, int dialCode) { this.code = code; this.name = name; this.dialCode = dialCode; } } static class CountryComparator implements Comparator { private Comparator comparator; CountryComparator() { comparator = Collator.getInstance(); } @SuppressWarnings("unchecked") @Override public int compare(Object o1, Object o2) { DuoEnrollController.Country c1 = (DuoEnrollController.Country) o1; DuoEnrollController.Country c2 = (DuoEnrollController.Country) o2; return comparator.compare(c1.name, c2.name); } } public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) { List<Map.Entry<K, V>> list = new LinkedList<>(map.entrySet()); Collections.sort(list, new Comparator<Map.Entry<K, V>>() { @Override public int compare(Map.Entry<K, V> o1, Map.Entry<K, V> o2) { return (o1.getValue()).compareTo(o2.getValue()); } }); Map<K, V> result = new LinkedHashMap<>(); for (Map.Entry<K, V> entry : list) { result.put(entry.getKey(), entry.getValue()); } return result; } }