001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.ui.login;
029
030import org.opencms.ade.configuration.CmsADEManager;
031import org.opencms.db.CmsLoginMessage;
032import org.opencms.db.CmsUserSettings;
033import org.opencms.file.CmsObject;
034import org.opencms.file.CmsProject;
035import org.opencms.file.CmsRequestContext;
036import org.opencms.file.CmsUser;
037import org.opencms.i18n.CmsMessageContainer;
038import org.opencms.i18n.CmsResourceBundleLoader;
039import org.opencms.jsp.CmsJspActionElement;
040import org.opencms.jsp.CmsJspLoginBean;
041import org.opencms.main.CmsBroadcast.ContentMode;
042import org.opencms.main.CmsException;
043import org.opencms.main.CmsLog;
044import org.opencms.main.OpenCms;
045import org.opencms.security.CmsCustomLoginException;
046import org.opencms.security.CmsRole;
047import org.opencms.security.CmsUserLog;
048import org.opencms.security.twofactor.CmsSecondFactorInfo;
049import org.opencms.security.twofactor.CmsTwoFactorAuthenticationHandler;
050import org.opencms.ui.A_CmsDialogContext;
051import org.opencms.ui.A_CmsUI;
052import org.opencms.ui.CmsVaadinUtils;
053import org.opencms.ui.I_CmsDialogContext;
054import org.opencms.ui.I_CmsDialogContext.ContextType;
055import org.opencms.ui.Messages;
056import org.opencms.ui.apps.CmsAppHierarchyConfiguration;
057import org.opencms.ui.apps.CmsAppWorkplaceUi;
058import org.opencms.ui.apps.CmsFileExplorerConfiguration;
059import org.opencms.ui.apps.CmsSitemapEditorConfiguration;
060import org.opencms.ui.components.CmsBasicDialog;
061import org.opencms.ui.components.CmsBasicDialog.DialogWidth;
062import org.opencms.ui.dialogs.CmsUserDataDialog;
063import org.opencms.ui.login.CmsLoginHelper.LoginParameters;
064import org.opencms.util.CmsStringUtil;
065import org.opencms.util.CmsUUID;
066import org.opencms.workplace.CmsLoginUserAgreement;
067import org.opencms.workplace.CmsWorkplace;
068import org.opencms.workplace.CmsWorkplaceLoginHandler;
069import org.opencms.workplace.CmsWorkplaceManager;
070import org.opencms.workplace.CmsWorkplaceSettings;
071
072import java.io.IOException;
073import java.text.SimpleDateFormat;
074import java.util.Collection;
075import java.util.Date;
076import java.util.List;
077import java.util.MissingResourceException;
078import java.util.ResourceBundle;
079
080import javax.servlet.http.HttpServletRequest;
081import javax.servlet.http.HttpServletResponse;
082import javax.servlet.http.HttpSession;
083
084import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
085import org.apache.commons.lang3.builder.ToStringStyle;
086import org.apache.commons.logging.Log;
087
088import com.vaadin.server.Page;
089import com.vaadin.server.VaadinRequest;
090import com.vaadin.server.VaadinService;
091import com.vaadin.server.VaadinServletRequest;
092import com.vaadin.server.VaadinServletResponse;
093import com.vaadin.ui.Component;
094import com.vaadin.ui.UI;
095import com.vaadin.ui.Window;
096
097/**
098 * Controller class which actually handles the login dialog logic.<p>
099 */
100public class CmsLoginController {
101
102    /**
103     * Represents the login target information.<p>
104     */
105    public static class CmsLoginTargetInfo {
106
107        /** The password. */
108        private String m_password;
109
110        /** The login target. */
111        private String m_target;
112
113        /** The user. */
114        private String m_user;
115
116        /**
117         * Creates a new instance.<p>
118         *
119         * @param target the login target
120         * @param user the user name
121         * @param password the password
122         */
123        public CmsLoginTargetInfo(String target, String user, String password) {
124
125            super();
126            m_target = target;
127            m_user = user;
128            m_password = password;
129        }
130
131        /**
132         * Returns the password.<p>
133         *
134         * @return the password
135         */
136        public String getPassword() {
137
138            return m_password;
139        }
140
141        /**
142         * Returns the target.<p>
143         *
144         * @return the target
145         */
146        public String getTarget() {
147
148            return m_target;
149        }
150
151        /**
152         * Returns the user.<p>
153         *
154         * @return the user
155         */
156        public String getUser() {
157
158            return m_user;
159        }
160    }
161
162    /**
163     * The login context.
164     */
165    public static class LoginContext {
166
167        /** The CMS context. */
168        private CmsObject m_cms;
169
170        /** The second factor information. */
171        private CmsSecondFactorInfo m_secondFactorInfo;
172
173        /** The user being logged in. */
174        private CmsUser m_user;
175
176        /**
177         * Gets the CmsObject.
178         *
179         * @return the CmsObject
180         */
181        public CmsObject getCms() {
182
183            return m_cms;
184        }
185
186        /**
187         * The second factor information for 2FA.
188         *
189         * @return the second factor information
190         */
191        public CmsSecondFactorInfo getSecondFactorInfo() {
192
193            return m_secondFactorInfo;
194        }
195
196        /**
197         * Gets the user to be logged in.
198         *
199         * @return the user
200         */
201        public CmsUser getUser() {
202
203            return m_user;
204        }
205
206        /**
207         * Sets the CMS context.
208         *
209         * @param cms the CMS context
210         */
211        public void setCms(CmsObject cms) {
212
213            m_cms = cms;
214        }
215
216        /**
217         * Sets the second factor information for 2FA.
218         *
219         * @param secondFactorInfo the second factor information
220         */
221        public void setSecondFactorInfo(CmsSecondFactorInfo secondFactorInfo) {
222
223            m_secondFactorInfo = secondFactorInfo;
224        }
225
226        /**
227         * Sets the user.
228         *
229         * @param user the user
230         */
231        public void setUser(CmsUser user) {
232
233            m_user = user;
234        }
235
236        /**
237         * @see java.lang.Object#toString()
238         */
239        @Override
240        public String toString() {
241
242            return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE);
243        }
244    };
245
246    /**
247     * Helper subclass of CmsLoginUserAgreement which can be used without a page context.<p>
248     *
249     * This is only used for detecting whether we need to display the user agreement dialog, not for displaying the dialog itself.<p>
250     */
251    protected static class UserAgreementHelper extends CmsLoginUserAgreement {
252
253        /** The replacement CMS context. */
254        private CmsObject m_cms;
255
256        /** The replacemenet workplace settings. */
257        private CmsWorkplaceSettings m_wpSettings;
258
259        /**
260         * Creates a new instance.<p>
261         *
262         * @param cms the replacement CMS context
263         * @param wpSettings the replacement workplace settings
264         */
265        public UserAgreementHelper(CmsObject cms, CmsWorkplaceSettings wpSettings) {
266
267            super(null);
268            m_cms = cms;
269            m_wpSettings = wpSettings;
270            initAcceptData();
271        }
272
273        /**
274         * @see org.opencms.workplace.CmsWorkplace#getCms()
275         */
276        @Override
277        public CmsObject getCms() {
278
279            return m_cms;
280        }
281
282        /**
283         * @see org.opencms.workplace.CmsWorkplace#getSettings()
284         */
285        @Override
286        public CmsWorkplaceSettings getSettings() {
287
288            return m_wpSettings;
289        }
290
291        /**
292         * @see org.opencms.workplace.CmsWorkplace#initWorkplaceMembers(org.opencms.jsp.CmsJspActionElement)
293         */
294        @Override
295        protected void initWorkplaceMembers(CmsJspActionElement jsp) {
296
297            // do nothing
298        }
299    }
300
301    /**
302     * Helper interface for splitting the actual login off into its own object
303     * which can be called from multiple places.
304     */
305    interface LoginContinuation {
306
307        /**
308         * Continues with the login process.
309         *
310         * @param data the login data
311         * @throws Exception if something goes wrong
312         */
313        void continueLogin(LoginContext data) throws Exception;
314    }
315
316    /** Additional info key to mark accounts as locked due to inactivity. */
317    public static final String KEY_ACCOUNT_LOCKED = "accountLocked";
318
319    /** The logger for this class. */
320    private static final Log LOG = CmsLog.getLog(CmsLoginController.class);
321
322    /** The two-factor authentication handler. */
323    protected CmsTwoFactorAuthenticationHandler m_otpHandler = OpenCms.getTwoFactorAuthenticationHandler();
324
325    /** The UI instance. */
326    CmsLoginUI m_ui;
327
328    /** The administrator CMS context. */
329    private CmsObject m_adminCms;
330
331    /** The parameters collected when the login app was opened. */
332    private LoginParameters m_params;
333
334    /***
335     * Creates a new instance.<p>
336     *
337     * @param adminCms the admin cms context
338     * @param params the parameters for the UI
339     */
340    public CmsLoginController(CmsObject adminCms, LoginParameters params) {
341
342        m_params = params;
343        m_adminCms = adminCms;
344    }
345
346    /**
347     * Returns the link to the login form.<p>
348     *
349     * @param cms the current cms context
350     *
351     * @return the login form link
352     */
353    public static String getFormLink(CmsObject cms) {
354
355        return OpenCms.getLinkManager().substituteLinkForUnknownTarget(
356            cms,
357            CmsWorkplaceLoginHandler.LOGIN_HANDLER,
358            false);
359    }
360
361    /**
362     * Gets the login target link.<p>
363     *
364     * @param currentCms the current CMS context
365     * @param settings the workplace settings
366     * @param requestedResource the requested resource parameter
367     *
368     * @return the login target
369     *
370     * @throws CmsException in case the user has insufficient permissions to access the login target
371     */
372    public static String getLoginTarget(CmsObject currentCms, CmsWorkplaceSettings settings, String requestedResource)
373    throws CmsException {
374
375        String directEditPath = CmsLoginHelper.getDirectEditPath(currentCms, settings.getUserSettings(), false);
376        String target = "";
377        boolean checkRole = false;
378        String fragment = UI.getCurrent() != null ? UI.getCurrent().getPage().getUriFragment() : "";
379        boolean workplace2 = false;
380
381        if ((requestedResource == null) && (directEditPath != null)) {
382            target = directEditPath;
383        } else if ((requestedResource != null) && !CmsWorkplace.JSP_WORKPLACE_URI.equals(requestedResource)) {
384            target = requestedResource;
385        } else {
386            workplace2 = true;
387            target = CmsVaadinUtils.getWorkplaceLink();
388            checkRole = true;
389        }
390
391        UserAgreementHelper userAgreementHelper = new UserAgreementHelper(currentCms, settings);
392        boolean showUserAgreement = userAgreementHelper.isShowUserAgreement();
393        if (showUserAgreement) {
394            target = userAgreementHelper.getConfigurationVfsPath()
395                + "?"
396                + CmsLoginUserAgreement.PARAM_WPRES
397                + "="
398                + target;
399        }
400        if (checkRole && !OpenCms.getRoleManager().hasRole(currentCms, CmsRole.WORKPLACE_USER)) {
401            workplace2 = false;
402            target = CmsLoginHelper.getDirectEditPath(currentCms, settings.getUserSettings(), true);
403            if (target == null) {
404                throw new CmsCustomLoginException(
405                    org.opencms.workplace.Messages.get().container(
406                        org.opencms.workplace.Messages.GUI_LOGIN_FAILED_NO_WORKPLACE_PERMISSIONS_0));
407            }
408        }
409        if (!workplace2) {
410            target = OpenCms.getLinkManager().substituteLink(currentCms, target);
411        }
412
413        if (workplace2 && CmsStringUtil.isEmptyOrWhitespaceOnly(fragment)) {
414            if (settings.getUserSettings().getStartView().startsWith("/")) {
415                if (CmsWorkplace.VIEW_WORKPLACE.equals(settings.getUserSettings().getStartView())) {
416                    fragment = CmsFileExplorerConfiguration.APP_ID;
417                } else if (CmsWorkplace.VIEW_ADMIN.equals(settings.getUserSettings().getStartView())) {
418                    fragment = CmsAppHierarchyConfiguration.APP_ID;
419                }
420            } else {
421                fragment = settings.getUserSettings().getStartView();
422            }
423        }
424
425        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(fragment)) {
426            if (CmsSitemapEditorConfiguration.APP_ID.equals(fragment)) {
427                target = OpenCms.getLinkManager().substituteLink(currentCms, CmsADEManager.PATH_SITEMAP_EDITOR_JSP)
428                    + "?path="
429                    + settings.getUserSettings().getStartFolder();
430            } else {
431                target += "#" + fragment;
432            }
433        }
434        return target;
435    }
436
437    /**
438     * Logs the current user out by invalidating the session an reloading the current URI.<p>
439     * Important:  This works only within vaadin apps.<p>
440     */
441    public static void logout() {
442
443        logout(false);
444    }
445
446    /**
447     * Logs the current user out by invalidating the session an reloading the current URI.<p>
448     * Important:  This works only within vaadin apps.<p>
449     *
450     * @param autoLogout true if this logout is triggered as part of the auto-logout function
451     */
452    public static void logout(boolean autoLogout) {
453
454        CmsObject cms = A_CmsUI.getCmsObject();
455        if (UI.getCurrent() instanceof CmsAppWorkplaceUi) {
456            ((CmsAppWorkplaceUi)UI.getCurrent()).onWindowClose();
457        }
458        String loggedInUser = cms.getRequestContext().getCurrentUser().getName();
459        UI.getCurrent().getSession().close();
460        String logoutUri = OpenCms.getLoginManager().getLogoutUri();
461        if (logoutUri != null) {
462            String target = OpenCms.getLinkManager().substituteLinkForUnknownTarget(cms, logoutUri, false);
463            target = OpenCms.getAuthorizationHandler().getLogoutRedirectUri(
464                cms,
465                (VaadinServletRequest)VaadinRequest.getCurrent(),
466                target);
467            // open in top frame, so it still works when the Vaadin dialog is embedded
468            Page.getCurrent().open(target, "_top", false);
469        } else {
470            String suffix = "";
471            if (autoLogout) {
472                suffix = "?" + CmsLoginHelper.PARAM_AUTOLOGOUT + "=true";
473            }
474            String loginLink = OpenCms.getLinkManager().substituteLinkForUnknownTarget(
475                cms,
476                CmsWorkplaceLoginHandler.LOGIN_HANDLER + suffix,
477                false);
478            loginLink = OpenCms.getAuthorizationHandler().getLogoutRedirectUri(
479                cms,
480                (VaadinServletRequest)VaadinRequest.getCurrent(),
481                loginLink);
482
483            VaadinService.getCurrentRequest().getWrappedSession().invalidate();
484            // open in top frame, so it still works when the Vaadin dialog is embedded
485            Page.getCurrent().open(loginLink, "_top", false);
486            // logout was successful
487            if (LOG.isInfoEnabled()) {
488                LOG.info(
489                    org.opencms.jsp.Messages.get().getBundle().key(
490                        org.opencms.jsp.Messages.LOG_LOGOUT_SUCCESFUL_3,
491                        loggedInUser,
492                        "{workplace logout option}",
493                        cms.getRequestContext().getRemoteAddress()));
494            }
495            CmsUserLog.logLogout(cms);
496        }
497    }
498
499    /**
500     * Logs out the current user redirecting to the login form afterwards.<p>
501     *
502     * @param cms the cms context
503     * @param request the servlet request
504     * @param response the servlet response
505     *
506     * @throws IOException if writing to the response fails
507     */
508    public static void logout(CmsObject cms, HttpServletRequest request, HttpServletResponse response)
509    throws IOException {
510
511        CmsUserLog.logLogout(cms);
512        String loggedInUser = cms.getRequestContext().getCurrentUser().getName();
513        HttpSession session = request.getSession(false);
514        String logoutUri = OpenCms.getLoginManager().getLogoutUri();
515        if (logoutUri == null) {
516            if (session != null) {
517                session.invalidate();
518                /* we need this because a new session might be created after this method,
519                 but before the session info is updated in OpenCmsCore.showResource. */
520                cms.getRequestContext().setUpdateSessionEnabled(false);
521            }
522            // logout was successful
523            if (LOG.isInfoEnabled()) {
524                LOG.info(
525                    org.opencms.jsp.Messages.get().getBundle().key(
526                        org.opencms.jsp.Messages.LOG_LOGOUT_SUCCESFUL_3,
527                        loggedInUser,
528                        cms.getRequestContext().addSiteRoot(cms.getRequestContext().getUri()),
529                        cms.getRequestContext().getRemoteAddress()));
530            }
531            String redirectUri = OpenCms.getAuthorizationHandler().getLogoutRedirectUri(cms, request, getFormLink(cms));
532            response.sendRedirect(redirectUri);
533        } else {
534            String redirectUri = OpenCms.getAuthorizationHandler().getLogoutRedirectUri(
535                cms,
536                request,
537                OpenCms.getLinkManager().substituteLinkForUnknownTarget(cms, logoutUri, false));
538            response.sendRedirect(redirectUri);
539        }
540    }
541
542    /**
543     * Gets the PC type.<p>
544     *
545     * @return the PC type
546     */
547    public String getPcType() {
548
549        String result = m_params.getPcType();
550        if (CmsStringUtil.isEmptyOrWhitespaceOnly(result)) {
551            result = "public";
552        }
553        return result;
554    }
555
556    /**
557     * Returns the reset password link.<p>
558     *
559     * @return the reset password link
560     */
561    public String getResetPasswordLink() {
562
563        String result = OpenCms.getLinkManager().substituteLinkForUnknownTarget(
564            CmsLoginUI.m_adminCms,
565            CmsWorkplaceLoginHandler.LOGIN_HANDLER,
566            false) + "?" + CmsLoginHelper.PARAM_RESET_PASSWORD;
567        String ou = m_ui.getOrgUnit();
568        result += "&" + CmsLoginHelper.PARAM_OUFQN + "=" + ou;
569        return result;
570    }
571
572    /**
573     * Returns true if the security option should be displayed in the login dialog.<p>
574     *
575     * @return true if the security option should be displayed in the login dialog
576     */
577    public boolean isShowSecure() {
578
579        return OpenCms.getLoginManager().isEnableSecurity();
580    }
581
582    /**
583     * Called when the user clicks on the 'forgot password' button.<p>
584     */
585    public void onClickForgotPassword() {
586
587        A_CmsUI.get().getPage().setLocation(getResetPasswordLink());
588    }
589
590    /**
591     * Called when the user clicks on the login button.<p>
592     */
593    public void onClickLogin() {
594
595        String user = m_ui.getUser();
596        String password = m_ui.getPassword();
597        String ou = m_ui.getOrgUnit();
598        if (CmsLoginOuSelector.OU_NONE.equals(ou)) {
599            displayError(
600                CmsVaadinUtils.getMessageText(Messages.GUI_LOGIN_NO_OU_SELECTED_WARNING_0) + "\n\n",
601                false,
602                false);
603            return;
604        }
605        {
606            CmsMessageContainer message = CmsLoginHelper.validateUserAndPasswordNotEmpty(user, password);
607            if (message != null) {
608                String errorMessage = message.key(m_params.getLocale());
609                displayError(errorMessage, true, false);
610                return;
611            }
612        }
613
614        String realUser = CmsStringUtil.joinPaths(ou, user);
615        String pcType = m_ui.getPcType();
616        CmsObject currentCms = A_CmsUI.getCmsObject();
617        CmsUser userObj = null;
618
619        try {
620            try {
621                userObj = currentCms.readUser(realUser);
622            } catch (CmsException e) {
623                LOG.warn(e.getLocalizedMessage(), e);
624                CmsMessageContainer message = org.opencms.workplace.Messages.get().container(
625                    org.opencms.workplace.Messages.GUI_LOGIN_FAILED_0);
626                displayError(message.key(m_params.getLocale()), true, true);
627                CmsUserLog.logLoginFailure(currentCms, realUser);
628                return;
629            }
630            final CmsUser userNonNull = userObj;
631            if (OpenCms.getLoginManager().canLockBecauseOfInactivity(currentCms, userObj)) {
632                boolean locked = null != userObj.getAdditionalInfo().get(KEY_ACCOUNT_LOCKED);
633                if (locked) {
634                    displayError(CmsInactiveUserMessages.getLockoutText(A_CmsUI.get().getLocale()), false, false);
635                    CmsUserLog.logLoginFailure(currentCms, realUser);
636                    return;
637                }
638            }
639
640            CmsObject cloneCms = OpenCms.initCmsObject(currentCms);
641            cloneCms.checkLoginUser(realUser, password);
642
643            String messageToChange = "";
644            if (OpenCms.getLoginManager().isPasswordReset(currentCms, userObj)) {
645                messageToChange = CmsVaadinUtils.getMessageText(Messages.GUI_PWCHANGE_RESET_0);
646            }
647            if (OpenCms.getLoginManager().requiresPasswordChange(currentCms, userObj)) {
648                messageToChange = getPasswordChangeMessage();
649            }
650            if (!CmsStringUtil.isEmptyOrWhitespaceOnly(messageToChange)) {
651                CmsChangePasswordDialog passwordDialog = new CmsChangePasswordDialog(
652                    currentCms,
653                    userObj,
654                    A_CmsUI.get().getLocale());
655                passwordDialog.setAdditionalMessage(messageToChange);
656                A_CmsUI.get().setContentToDialog(
657                    Messages.get().getBundle(A_CmsUI.get().getLocale()).key(Messages.GUI_PWCHANGE_HEADER_0)
658                        + userObj.getSimpleName(),
659                    passwordDialog);
660                return;
661            }
662
663            /*
664             * Login branches now into multiple paths: For the non-OTP case we can do it directly (synchronously),
665             * and for the OTP cases we need to show additional dialogs first (so it's asynchronous). But at the end,
666             * the same things happen. So we put these 'things that happen at the end' in a closure, so we can either
667             * execute it directly or pass it as a callback to the dialogs.
668             */
669            LoginContinuation loginContinuation = (LoginContext details) -> {
670
671                // provisional login successful, now do for real
672                // we use another separate CmsObject so we can manually control when it is written to the session info
673                CmsObject loginCms = OpenCms.initCmsObject(currentCms);
674                CmsSecondFactorInfo secondFactorInfo = details.getSecondFactorInfo();
675                loginCms.loginUser(realUser, password, secondFactorInfo);
676                CmsUserLog.logLogin(loginCms, realUser);
677
678                if (LOG.isInfoEnabled()) {
679                    CmsRequestContext context = loginCms.getRequestContext();
680                    LOG.info(
681                        org.opencms.jsp.Messages.get().getBundle().key(
682                            org.opencms.jsp.Messages.LOG_LOGIN_SUCCESSFUL_3,
683                            context.getCurrentUser().getName(),
684                            "{workplace login dialog}",
685                            context.getRemoteAddress()));
686                }
687                CmsWorkplaceSettings settings = CmsLoginHelper.initSiteAndProject(loginCms);
688                final String loginTarget = getLoginTarget(loginCms, settings, m_params.getRequestedResource());
689
690                CmsLoginHelper.setCookieData(
691                    pcType,
692                    user,
693                    ou,
694                    (VaadinServletRequest)(VaadinService.getCurrentRequest()),
695                    (VaadinServletResponse)(VaadinService.getCurrentResponse()));
696                VaadinService.getCurrentRequest().getWrappedSession().setAttribute(
697                    CmsWorkplaceManager.SESSION_WORKPLACE_SETTINGS,
698                    settings);
699
700                final boolean isPublicPC = CmsLoginForm.PC_TYPE_PUBLIC.equals(pcType);
701                if (OpenCms.getLoginManager().requiresUserDataCheck(loginCms, userNonNull)) {
702                    I_CmsDialogContext context = new A_CmsDialogContext("", ContextType.appToolbar, null) {
703
704                        @Override
705                        public void finish(CmsProject project, String siteRoot) {
706
707                            finish(null);
708                        }
709
710                        @Override
711                        public void finish(Collection<CmsUUID> result) {
712
713                            initSessionAndSendMessages(currentCms, loginCms);
714                            m_ui.openLoginTarget(loginTarget, isPublicPC);
715
716                        }
717
718                        public void focus(CmsUUID structureId) {
719
720                            // nothing to do
721                        }
722
723                        public List<CmsUUID> getAllStructureIdsInView() {
724
725                            return null;
726                        }
727
728                        @Override
729                        public CmsObject getCms() {
730
731                            return loginCms;
732                        }
733
734                        @Override
735                        public void start(String title, Component dialog, DialogWidth style) {
736
737                            if (dialog != null) {
738                                m_window = CmsBasicDialog.prepareWindow(style);
739                                m_window.setCaption(title);
740                                m_window.setContent(dialog);
741                                UI.getCurrent().addWindow(m_window);
742                                if (dialog instanceof CmsBasicDialog) {
743                                    ((CmsBasicDialog)dialog).initActionHandler(m_window);
744                                }
745                            }
746                        }
747
748                        public void updateUserInfo() {
749
750                            // not supported
751                        }
752                    };
753                    CmsUser u = currentCms.readUser(userNonNull.getId());
754                    u.setAdditionalInfo(
755                        CmsUserSettings.ADDITIONAL_INFO_LAST_USER_DATA_CHECK,
756                        Long.toString(System.currentTimeMillis()));
757                    loginCms.writeUser(u);
758                    CmsUserDataDialog dialog = new CmsUserDataDialog(context, true);
759                    context.start(dialog.getTitle(UI.getCurrent().getLocale()), dialog);
760                } else {
761                    initSessionAndSendMessages(currentCms, loginCms);
762                    m_ui.openLoginTarget(loginTarget, isPublicPC);
763                }
764            };
765            LoginContext context = new LoginContext();
766            context.setUser(userNonNull);
767            context.setCms(currentCms);
768            if (m_otpHandler.needsTwoFactorAuthentication(userNonNull)) {
769                m_ui.clearError();
770                if (!m_otpHandler.hasSecondFactor(userObj)) {
771                    showSecondFactorSetup(context, loginContinuation);
772                } else {
773                    showSecondFactorDialog(context, loginContinuation);
774                }
775            } else {
776                loginContinuation.continueLogin(context);
777            }
778        } catch (Exception e) {
779
780            handleError(currentCms, realUser, e);
781        }
782    }
783
784    /**
785     * Called on initialization.<p>
786     */
787    public void onInit() {
788
789        String authToken = m_params.getAuthToken();
790        if (authToken != null) {
791            m_ui.showForgotPasswordView(authToken);
792        } else if (m_params.isReset()) {
793            m_ui.showPasswordResetDialog(m_params.getOufqn());
794        } else {
795            CmsUser user = A_CmsUI.getCmsObject().getRequestContext().getCurrentUser();
796            boolean loggedIn = !user.isGuestUser();
797            m_ui.setSelectableOrgUnits(CmsLoginHelper.getOrgUnitsForLoginDialog(A_CmsUI.getCmsObject(), null));
798            if (loggedIn) {
799                if (m_params.isLogout()) {
800                    logout(false);
801                } else {
802                    if (CmsLoginHelper.shouldAutoLogout(A_CmsUI.getCmsObject())) {
803                        logout(true);
804                    } else {
805                        m_ui.showAlreadyLoggedIn();
806                    }
807                }
808            } else {
809                m_ui.showLoginView(m_params.getOufqn(), m_params.isAutoLogout());
810            }
811        }
812
813    }
814
815    /**
816     * Sets the login ui reference.<p>
817     *
818     * @param ui the login ui
819     */
820    public void setUi(CmsLoginUI ui) {
821
822        m_ui = ui;
823    }
824
825    /**
826     * Returns the message to be displayed for the user data check dialog.<p>
827     *
828     * @return the message to display
829     */
830    protected String getPasswordChangeMessage() {
831
832        ResourceBundle bundle = null;
833        try {
834            bundle = CmsResourceBundleLoader.getBundle("org.opencms.passwordchange.custom", A_CmsUI.get().getLocale());
835            return bundle.getString("passwordchange.text");
836        } catch (MissingResourceException e) {
837            return CmsVaadinUtils.getMessageText(Messages.GUI_PWCHANGE_INTERVAL_EXPIRED_0);
838        }
839    }
840
841    /**
842     * Handles exceptions during the login process and displays appropriate error messages.
843     *
844     * @param currentCms the CMS context
845     * @param user the user being logged in
846     * @param e the error
847     */
848    protected void handleError(CmsObject currentCms, String user, Exception e) {
849
850        CmsMessageContainer message = null;
851
852        // there was an error during login
853        if (e instanceof CmsException) {
854            CmsMessageContainer exceptionMessage = ((CmsException)e).getMessageContainer();
855            if (org.opencms.security.Messages.ERR_LOGIN_FAILED_DISABLED_2 == exceptionMessage.getKey()) {
856                // the user account is disabled
857                message = org.opencms.workplace.Messages.get().container(
858                    org.opencms.workplace.Messages.GUI_LOGIN_FAILED_DISABLED_0);
859            } else if (org.opencms.security.Messages.ERR_LOGIN_FAILED_TEMP_DISABLED_4 == exceptionMessage.getKey()) {
860                // the user account is temporarily disabled because of too many login failures
861                message = org.opencms.workplace.Messages.get().container(
862                    org.opencms.workplace.Messages.GUI_LOGIN_FAILED_TEMP_DISABLED_0);
863            } else if (org.opencms.security.Messages.ERR_LOGIN_FAILED_WITH_MESSAGE_1 == exceptionMessage.getKey()) {
864                // all logins have been disabled be the Administration
865                CmsLoginMessage loginMessage2 = OpenCms.getLoginManager().getLoginMessage();
866                if (loginMessage2 != null) {
867                    message = org.opencms.workplace.Messages.get().container(
868                        org.opencms.workplace.Messages.GUI_LOGIN_FAILED_WITH_MESSAGE_1,
869                        loginMessage2.getMessage().replace("\n", ""));
870                }
871            }
872        }
873        if (message == null) {
874            if (e instanceof CmsCustomLoginException) {
875                message = ((CmsCustomLoginException)e).getMessageContainer();
876            } else {
877                // any other error - display default message
878                message = org.opencms.workplace.Messages.get().container(
879                    org.opencms.workplace.Messages.GUI_LOGIN_FAILED_0);
880                LOG.warn(e.getLocalizedMessage(), e);
881                displayError(message.key(m_params.getLocale()), true, true);
882                CmsUserLog.logLoginFailure(currentCms, user);
883                return;
884            }
885        }
886
887        if (e instanceof CmsException) {
888            CmsJspLoginBean.logLoginException(currentCms.getRequestContext(), user, (CmsException)e);
889            CmsUserLog.logLoginFailure(currentCms, user);
890        } else {
891            LOG.error(e.getLocalizedMessage(), e);
892        }
893        displayError(message.key(m_params.getLocale()), false, false);
894    }
895
896    /**
897     * Switches the session to a new one with the logged in CmsObject.
898     *
899     * <p>This needs to be called in the <em>last</em> request to the Vaadin servlet in the login process, because switching the session breaks the Vaadin session state.
900     *
901     * @param currentCms the CmsObject for the current request from the Vaadin UI
902     * @param loginCms the CmsObject which was used for the actual login operation
903     */
904    protected void initSessionAndSendMessages(CmsObject currentCms, CmsObject loginCms) {
905
906        HttpSession session = ((HttpServletRequest)VaadinService.getCurrentRequest()).getSession(false);
907        if (session != null) {
908            session.invalidate();
909        }
910        session = ((HttpServletRequest)VaadinService.getCurrentRequest()).getSession(true);
911
912        // we don't want currentCms to be used to automatically update the session at the end of the request...
913        currentCms.getRequestContext().setUpdateSessionEnabled(false);
914
915        // ...instead we manually update the session with loginCms
916        loginCms.getRequestContext().setUpdateSessionEnabled(true);
917        OpenCms.getSessionManager().updateSessionInfo(loginCms, (HttpServletRequest)VaadinService.getCurrentRequest());
918
919        String storedMessage = null;
920        CmsLoginMessage loginMessage = OpenCms.getLoginManager().getLoginMessage();
921        if (loginMessage != null) {
922            // forbidden implies active
923            if (loginMessage.isLoginCurrentlyForbidden()) {
924                // we are an administrator, otherwise login would have failed
925                if (loginMessage.getTimeEnd() == CmsLoginMessage.DEFAULT_TIME_END) {
926                    storedMessage = org.opencms.workplace.Messages.get().container(
927                        org.opencms.workplace.Messages.GUI_LOGIN_SUCCESS_WITH_MESSAGE_WITHOUT_TIME_1,
928                        loginMessage.getMessage(),
929                        new Date(loginMessage.getTimeEnd())).key(A_CmsUI.get().getLocale());
930                } else {
931                    storedMessage = org.opencms.workplace.Messages.get().container(
932                        org.opencms.workplace.Messages.GUI_LOGIN_SUCCESS_WITH_MESSAGE_2,
933                        loginMessage.getMessage(),
934                        new Date(loginMessage.getTimeEnd())).key(A_CmsUI.get().getLocale());
935                }
936            } else if (loginMessage.isActive()) {
937                storedMessage = loginMessage.getMessage();
938            }
939        }
940
941        if (storedMessage != null) {
942            OpenCms.getSessionManager().sendBroadcast(
943                null,
944                storedMessage,
945                loginCms.getRequestContext().getCurrentUser(),
946                ContentMode.html);
947        }
948    }
949
950    /**
951     * Gets the CMS context.<p>
952     *
953     * @return the CMS context
954     */
955    CmsObject getCms() {
956
957        return m_adminCms;
958    }
959
960    /**
961     * Displays the given error message.<p>
962     *
963     * @param message the message
964     * @param showForgotPassword in case the forgot password link should be shown
965     * @param showTime show the time
966     */
967    private void displayError(String message, boolean showForgotPassword, boolean showTime) {
968
969        message = message.replace("\n", "<br />");
970        if (showForgotPassword) {
971            message += "<br /><br /><a href=\""
972                + getResetPasswordLink()
973                + "\">"
974                + CmsVaadinUtils.getMessageText(Messages.GUI_FORGOT_PASSWORD_0)
975                + "</a>";
976        }
977        if (showTime) {
978            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
979            message += "<div style=\"position:absolute;right:6px;bottom:5px;\">"
980                + CmsVaadinUtils.getMessageText(Messages.GUI_TIME_1, sdf.format(new Date()))
981                + "</div>";
982        }
983        m_ui.showLoginError(message);
984    }
985
986    /**
987     * Shows the verification dialog for 2FA.
988     *
989     * @param context the login context
990     * @param loginContinuation the handler to which we pass the verification code to continue with the login
991     */
992    private void showSecondFactorDialog(LoginContext context, LoginContinuation loginContinuation) {
993
994        Window window = CmsBasicDialog.prepareWindow(DialogWidth.narrow);
995        window.setClosable(false);
996        window.setResizable(false);
997        window.setCaption(CmsSecondFactorDialog.getCaption(context.getUser()));
998        CmsSecondFactorDialog dialog = new CmsSecondFactorDialog(context.getUser(), verificationCode -> {
999            context.setSecondFactorInfo(new CmsSecondFactorInfo(verificationCode));
1000            try {
1001                loginContinuation.continueLogin(context);
1002            } catch (Exception e) {
1003                handleError(context.getCms(), context.getUser().getName(), e);
1004            }
1005        });
1006        A_CmsUI.get().addWindow(window);
1007        window.setContent(dialog);
1008
1009    }
1010
1011    /**
1012     * Shows a dialog for setting up 2FA.
1013     *
1014     * @param context the login context
1015     * @param loginContinuation the handler we call with the secret and verification code to set up 2FA and proceed with the loin.
1016     */
1017    private void showSecondFactorSetup(LoginContext context, LoginContinuation loginContinuation) {
1018
1019        Window window = CmsBasicDialog.prepareWindow(DialogWidth.wide);
1020        window.setClosable(false);
1021        window.setResizable(false);
1022        CmsSecondFactorSetupDialog dialog = new CmsSecondFactorSetupDialog(context, context2 -> {
1023            try {
1024                loginContinuation.continueLogin(context);
1025            } catch (Exception e) {
1026                handleError(context.getCms(), context.getUser().getName(), e);
1027            }
1028        });
1029        window.setCaption(CmsVaadinUtils.getMessageText(Messages.GUI_LOGIN_2FA_SETUP_0));
1030        A_CmsUI.get().addWindow(window);
1031        window.setContent(dialog);
1032
1033    }
1034}