/* * Password Management Servlets (PWM) * http://www.pwm-project.org * * Copyright (c) 2006-2009 Novell, Inc. * Copyright (c) 2009-2017 The PWM Project * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package password.pwm.http.servlet; import com.novell.ldapchai.exception.ChaiUnavailableException; import net.glxn.qrgen.QRCode; import password.pwm.AppProperty; import password.pwm.Permission; import password.pwm.PwmApplication; import password.pwm.PwmConstants; import password.pwm.bean.LoginInfoBean; import password.pwm.bean.UserIdentity; import password.pwm.bean.UserInfoBean; import password.pwm.config.Configuration; import password.pwm.config.PwmSetting; import password.pwm.config.option.ForceSetupPolicy; import password.pwm.error.ErrorInformation; import password.pwm.error.PwmError; import password.pwm.error.PwmException; import password.pwm.error.PwmOperationalException; import password.pwm.error.PwmUnrecoverableException; import password.pwm.http.HttpMethod; import password.pwm.http.JspUrl; import password.pwm.http.PwmRequest; import password.pwm.http.PwmRequestAttribute; import password.pwm.http.PwmSession; import password.pwm.http.bean.SetupOtpBean; import password.pwm.ldap.auth.AuthenticationType; import password.pwm.svc.PwmService; import password.pwm.svc.event.AuditEvent; import password.pwm.svc.event.AuditRecordFactory; import password.pwm.svc.event.UserAuditRecord; import password.pwm.svc.stats.Statistic; import password.pwm.util.java.JavaHelper; import password.pwm.util.java.JsonUtil; import password.pwm.util.java.StringUtil; import password.pwm.util.Validator; import password.pwm.util.logging.PwmLogger; import password.pwm.util.operations.OtpService; import password.pwm.util.operations.otp.OTPUserRecord; import password.pwm.ws.server.RestResultBean; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; /** * User interaction servlet for setting up OTP secret * * @author Jason D. Rivard, Menno Pieters */ @WebServlet( name="SetupOtpServlet", urlPatterns={ PwmConstants.URL_PREFIX_PRIVATE + "/setup-otp", PwmConstants.URL_PREFIX_PRIVATE + "/SetupOtp" } ) public class SetupOtpServlet extends AbstractPwmServlet { private static final PwmLogger LOGGER = PwmLogger.forClass(SetupOtpServlet.class); enum SetupOtpAction implements AbstractPwmServlet.ProcessAction { clearOtp(HttpMethod.POST), testOtpSecret(HttpMethod.POST), toggleSeen(HttpMethod.POST), restValidateCode(HttpMethod.POST), complete(HttpMethod.POST), skip(HttpMethod.POST), ; private final HttpMethod method; SetupOtpAction(final HttpMethod method) { this.method = method; } public Collection<HttpMethod> permittedMethods() { return Collections.singletonList(method); } } protected SetupOtpAction readProcessAction(final PwmRequest request) throws PwmUnrecoverableException { try { return SetupOtpAction.valueOf(request.readParameterAsString(PwmConstants.PARAM_ACTION_REQUEST)); } catch (IllegalArgumentException e) { return null; } } protected void processAction(final PwmRequest pwmRequest) throws ServletException, ChaiUnavailableException, IOException, PwmUnrecoverableException { // fetch the required beans / managers final PwmApplication pwmApplication = pwmRequest.getPwmApplication(); final PwmSession pwmSession = pwmRequest.getPwmSession(); final UserInfoBean uiBean = pwmSession.getUserInfoBean(); final Configuration config = pwmApplication.getConfig(); if (!config.readSettingAsBoolean(PwmSetting.OTP_ENABLED)) { final String errorMsg = "setup OTP Secret service is not enabled"; final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_SERVICE_NOT_AVAILABLE, errorMsg); LOGGER.error(pwmRequest, errorInformation); pwmRequest.respondWithError(errorInformation); return; } // check to see if the user is permitted to setup OTP if (!pwmSession.getSessionManager().checkPermission(pwmApplication, Permission.SETUP_OTP_SECRET)) { final String errorMsg = String.format("user %s does not have permission to setup an OTP secret", uiBean.getUserIdentity()); final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNAUTHORIZED, errorMsg); LOGGER.error(pwmRequest, errorInformation); pwmRequest.respondWithError(errorInformation); return; } // check whether the setup can be stored if (!canSetupOtpSecret(config)) { LOGGER.error(pwmSession, "OTP Secret cannot be setup"); pwmRequest.respondWithError(PwmError.ERROR_INVALID_CONFIG.toInfo()); return; } if (pwmSession.getLoginInfoBean().getType() == AuthenticationType.AUTH_WITHOUT_PASSWORD) { LOGGER.error(pwmSession, "OTP Secret requires a password login"); throw new PwmUnrecoverableException(PwmError.ERROR_PASSWORD_REQUIRED); } final SetupOtpBean otpBean = pwmApplication.getSessionStateService().getBean(pwmRequest, SetupOtpBean.class); initializeBean(pwmRequest, otpBean); final SetupOtpAction action = readProcessAction(pwmRequest); if (action != null) { pwmRequest.validatePwmFormID(); switch (action) { case clearOtp: handleClearOtpSecret(pwmRequest, otpBean); break; case testOtpSecret: handleTestOtpSecret(pwmRequest, otpBean); break; case toggleSeen: otpBean.setCodeSeen(!otpBean.isCodeSeen()); break; case restValidateCode: handleRestValidateCode(pwmRequest); return; case complete: handleComplete(pwmRequest); return; case skip: handleSkip(pwmRequest, otpBean); return; default: JavaHelper.unhandledSwitchStatement(action); } } this.advanceToNextStage(pwmRequest, otpBean); } private void advanceToNextStage( final PwmRequest pwmRequest, final SetupOtpBean otpBean ) throws PwmUnrecoverableException, IOException, ServletException, ChaiUnavailableException { if (otpBean.isHasPreExistingOtp()) { pwmRequest.forwardToJsp(JspUrl.SETUP_OTP_SECRET_EXISTING); return; } final PwmApplication pwmApplication = pwmRequest.getPwmApplication(); final PwmSession pwmSession = pwmRequest.getPwmSession(); if (otpBean.isConfirmed()) { final OtpService otpService = pwmApplication.getOtpService(); final UserIdentity theUser = pwmSession.getUserInfoBean().getUserIdentity(); try { otpService.writeOTPUserConfiguration( pwmSession, theUser, otpBean.getOtpUserRecord() ); otpBean.setWritten(true); // Update the current user info bean, so the user can check the code right away pwmSession.getUserInfoBean().setOtpUserRecord(otpBean.getOtpUserRecord()); // mark the event log final UserAuditRecord auditRecord = new AuditRecordFactory(pwmRequest).createUserAuditRecord( AuditEvent.SET_OTP_SECRET, pwmSession.getUserInfoBean(), pwmSession ); pwmApplication.getAuditManager().submit(auditRecord); if (pwmApplication.getStatisticsManager() != null && pwmApplication.getStatisticsManager().status() == PwmService.STATUS.OPEN) { pwmApplication.getStatisticsManager().incrementValue(Statistic.SETUP_OTP_SECRET); } } catch (Exception e) { final ErrorInformation errorInformation; if (e instanceof PwmException) { errorInformation = ((PwmException) e).getErrorInformation(); } else { errorInformation = new ErrorInformation(PwmError.ERROR_WRITING_OTP_SECRET,"unexpected error saving otp secret: " + e.getMessage()); } LOGGER.error(pwmSession, errorInformation.toDebugStr()); setLastError(pwmRequest, errorInformation); } } if (otpBean.isCodeSeen()) { if (otpBean.isWritten()) { pwmRequest.forwardToJsp(JspUrl.SETUP_OTP_SECRET_SUCCESS); } else { pwmRequest.forwardToJsp(JspUrl.SETUP_OTP_SECRET_TEST); } } else { final String qrCodeValue = makeQrCodeDataImageUrl(pwmRequest, otpBean.getOtpUserRecord()); pwmRequest.setAttribute(PwmRequestAttribute.SetupOtp_QrCodeValue, qrCodeValue); pwmRequest.forwardToJsp(JspUrl.SETUP_OTP_SECRET); } } private void handleSkip( final PwmRequest pwmRequest, final SetupOtpBean otpBean ) throws PwmUnrecoverableException, IOException, ServletException, ChaiUnavailableException { boolean allowSkip = false; if (!pwmRequest.isForcedPageView()) { allowSkip = true; } else { final ForceSetupPolicy policy = pwmRequest.getConfig().readSettingAsEnum(PwmSetting.OTP_FORCE_SETUP, ForceSetupPolicy.class); if (policy == ForceSetupPolicy.FORCE_ALLOW_SKIP) { allowSkip = true; } } if (allowSkip) { pwmRequest.getPwmSession().getUserInfoBean().setRequiresOtpConfig(false); pwmRequest.sendRedirectToContinue(); return; } this.advanceToNextStage(pwmRequest, otpBean); } private void handleComplete( final PwmRequest pwmRequest ) throws PwmUnrecoverableException, IOException, ServletException, ChaiUnavailableException { final PwmSession pwmSession = pwmRequest.getPwmSession(); pwmSession.getLoginInfoBean().setFlag(LoginInfoBean.LoginFlag.skipOtp); pwmRequest.getPwmApplication().getSessionStateService().clearBean(pwmRequest, SetupOtpBean.class); pwmRequest.sendRedirectToContinue(); } private void handleRestValidateCode( final PwmRequest pwmRequest ) throws PwmUnrecoverableException, IOException, ServletException, ChaiUnavailableException { final PwmApplication pwmApplication = pwmRequest.getPwmApplication(); final PwmSession pwmSession = pwmRequest.getPwmSession(); final OTPUserRecord otpUserRecord = pwmSession.getUserInfoBean().getOtpUserRecord(); final OtpService otpService = pwmApplication.getOtpService(); final String bodyString = pwmRequest.readRequestBodyAsString(); final Map<String, String> clientValues = JsonUtil.deserializeStringMap(bodyString); final String code = Validator.sanitizeInputValue(pwmApplication.getConfig(), clientValues.get("code"), 1024); try { final boolean passed = otpService.validateToken( pwmSession, pwmSession.getUserInfoBean().getUserIdentity(), otpUserRecord, code, false ); final RestResultBean restResultBean = new RestResultBean(); restResultBean.setData(passed); LOGGER.trace(pwmSession,"returning result for restValidateCode: " + JsonUtil.serialize(restResultBean)); pwmRequest.outputJsonResult(restResultBean); } catch (PwmOperationalException e) { final String errorMsg = "error during otp code validation: " + e.getMessage(); LOGGER.error(pwmSession, errorMsg); pwmRequest.outputJsonResult(RestResultBean.fromError(new ErrorInformation(PwmError.ERROR_UNKNOWN,errorMsg),pwmRequest)); } } private void handleClearOtpSecret( final PwmRequest pwmRequest, final SetupOtpBean otpBean ) throws PwmUnrecoverableException, IOException, ServletException, ChaiUnavailableException { final PwmApplication pwmApplication = pwmRequest.getPwmApplication(); final PwmSession pwmSession = pwmRequest.getPwmSession(); final OtpService service = pwmApplication.getOtpService(); final UserIdentity theUser = pwmSession.getUserInfoBean().getUserIdentity(); try { service.clearOTPUserConfiguration(pwmSession, theUser); } catch (PwmOperationalException e) { setLastError(pwmRequest, e.getErrorInformation()); LOGGER.error(pwmRequest, e.getErrorInformation()); return; } otpBean.setHasPreExistingOtp(false); initializeBean(pwmRequest, otpBean); } private void handleTestOtpSecret( final PwmRequest pwmRequest, final SetupOtpBean otpBean ) throws PwmUnrecoverableException, ChaiUnavailableException, IOException, ServletException { final PwmApplication pwmApplication = pwmRequest.getPwmApplication(); final PwmSession pwmSession = pwmRequest.getPwmSession(); final String otpToken = pwmRequest.readParameterAsString(PwmConstants.PARAM_OTP_TOKEN); final OtpService otpService = pwmApplication.getOtpService(); if (otpToken != null && otpToken.length() > 0) { try { if (pwmRequest.getConfig().isDevDebugMode()) { LOGGER.trace(pwmRequest, "testing against otp record: " + JsonUtil.serialize(otpBean.getOtpUserRecord())); } if (otpService.validateToken( pwmSession, pwmSession.getUserInfoBean().getUserIdentity(), otpBean.getOtpUserRecord(), otpToken, false )) { LOGGER.debug(pwmRequest, "test OTP token returned true, valid OTP secret provided"); otpBean.setConfirmed(true); otpBean.setChallenge(null); } else { LOGGER.debug(pwmRequest, "test OTP token returned false, incorrect OTP secret provided"); setLastError(pwmRequest, new ErrorInformation(PwmError.ERROR_TOKEN_INCORRECT)); } } catch (PwmOperationalException e) { LOGGER.error(pwmRequest, "error validating otp token: " + e.getMessage()); setLastError(pwmRequest, e.getErrorInformation()); } } //@todo: handle case to HOTP } private void initializeBean( final PwmRequest pwmRequest, final SetupOtpBean otpBean ) throws PwmUnrecoverableException, ChaiUnavailableException { final PwmApplication pwmApplication = pwmRequest.getPwmApplication(); final PwmSession pwmSession = pwmRequest.getPwmSession(); // has pre-existing, nothing to do. if (otpBean.isHasPreExistingOtp()) { return; } final OtpService service = pwmApplication.getOtpService(); final UserIdentity theUser = pwmSession.getUserInfoBean().getUserIdentity(); // first time here if (otpBean.getOtpUserRecord() == null) { final OTPUserRecord existingUserRecord = service.readOTPUserConfiguration(pwmRequest.getSessionLabel(),theUser); if (existingUserRecord != null) { otpBean.setHasPreExistingOtp(true); LOGGER.trace(pwmSession, "user has existing otp record"); return; } } // make a new user record. if (otpBean.getOtpUserRecord() == null) { try { final Configuration config = pwmApplication.getConfig(); final String identifierConfigValue = config.readSettingAsString(PwmSetting.OTP_SECRET_IDENTIFIER); final String identifier = pwmSession.getSessionManager().getMacroMachine(pwmApplication).expandMacros(identifierConfigValue); final OTPUserRecord otpUserRecord = new OTPUserRecord(); final List<String> rawRecoveryCodes = pwmApplication.getOtpService().initializeUserRecord( otpUserRecord, pwmRequest.getSessionLabel(), identifier ); otpBean.setOtpUserRecord(otpUserRecord); otpBean.setRecoveryCodes(rawRecoveryCodes); LOGGER.trace(pwmSession, "generated new otp record"); if (config.isDevDebugMode()) { LOGGER.trace(pwmRequest, "newly generated otp record: " + JsonUtil.serialize(otpUserRecord)); } } catch (Exception e) { final String errorMsg = "error setting up new OTP secret: " + e.getMessage(); LOGGER.error(pwmSession, errorMsg); throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNKNOWN, errorMsg)); } } } private boolean canSetupOtpSecret(final Configuration config) { /* TODO */ return true; } private static String makeQrCodeDataImageUrl( final PwmRequest pwmRequest, final OTPUserRecord otpUserRecord ) throws PwmUnrecoverableException { final String otpTypeValue = otpUserRecord.getType().toString().toLowerCase(); final String identifier = StringUtil.urlEncode(otpUserRecord.getIdentifier()); final String secret = StringUtil.urlEncode(otpUserRecord.getSecret()); final String qrCodeContent = "otpauth://" + otpTypeValue + "/" + identifier + "?secret=" + secret; final int height = Integer.parseInt(pwmRequest.getConfig().readAppProperty(AppProperty.OTP_QR_IMAGE_HEIGHT)); final int width = Integer.parseInt(pwmRequest.getConfig().readAppProperty(AppProperty.OTP_QR_IMAGE_WIDTH)); final byte[] imageBytes; try { imageBytes = QRCode.from(qrCodeContent) .withCharset(PwmConstants.DEFAULT_CHARSET.toString()) .withSize(width, height) .stream() .toByteArray(); } catch (Exception e) { final String errorMsg = "error generating qrcode image: " + e.getMessage() + ", payload length=" + qrCodeContent.length(); LOGGER.error(pwmRequest, errorMsg, e); throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNKNOWN, errorMsg)); } return "data:image/png;base64," + StringUtil.base64Encode(imageBytes); } }