View Javadoc
1   /*
2    * Copyright 2012-2025 CodeLibs Project and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
15   */
16  package org.codelibs.fess.app.web.login;
17  
18  import org.apache.logging.log4j.LogManager;
19  import org.apache.logging.log4j.Logger;
20  import org.codelibs.core.lang.StringUtil;
21  import org.codelibs.fess.app.service.UserService;
22  import org.codelibs.fess.app.web.base.FessLoginAction;
23  import org.codelibs.fess.app.web.base.login.LocalUserCredential;
24  import org.codelibs.fess.app.web.profile.ProfileAction;
25  import org.codelibs.fess.mylasta.action.FessMessages;
26  import org.codelibs.fess.util.ComponentUtil;
27  import org.codelibs.fess.util.RenderDataUtil;
28  import org.dbflute.optional.OptionalEntity;
29  import org.dbflute.optional.OptionalThing;
30  import org.lastaflute.web.Execute;
31  import org.lastaflute.web.login.exception.LoginFailureException;
32  import org.lastaflute.web.response.HtmlResponse;
33  import org.lastaflute.web.validation.VaErrorHook;
34  
35  import jakarta.annotation.Resource;
36  import jakarta.servlet.http.HttpSession;
37  
38  /**
39   * The login action.
40   */
41  public class LoginAction extends FessLoginAction {
42  
43      /**
44       * Default constructor.
45       */
46      public LoginAction() {
47          super();
48      }
49  
50      private static final Logger logger = LogManager.getLogger(LoginAction.class);
51  
52      private static final String INVALID_OLD_PASSWORD = "LoginAction.invalidOldPassword";
53  
54      // ===================================================================================
55      // Attribute
56      //
57      @Resource
58      private UserService userService;
59  
60      // ===================================================================================
61      //                                                                       Login Execute
62      //                                                                      ==============
63  
64      /**
65       * Displays the login page.
66       *
67       * @return the HTML response for the login page
68       */
69      @Execute
70      public HtmlResponse index() {
71          getSession().ifPresent(session -> session.removeAttribute(INVALID_OLD_PASSWORD));
72          return asIndexPage(null).useForm(LoginForm.class);
73      }
74  
75      private HtmlResponse asIndexPage(final LoginForm form) {
76          if (form != null) {
77              form.clearSecurityInfo();
78          }
79          return asHtml(virtualHost(path_Login_IndexJsp)).renderWith(data -> {
80              RenderDataUtil.register(data, "notification", fessConfig.getNotificationLogin());
81              saveToken();
82          });
83      }
84  
85      /**
86       * Handles user login with the provided credentials.
87       *
88       * @param form the login form containing username and password
89       * @return the HTML response after login attempt
90       */
91      @Execute
92      public HtmlResponse login(final LoginForm form) {
93          validate(form, messages -> {}, () -> asIndexPage(form));
94          verifyToken(() -> asIndexPage(form));
95          final String username = form.username;
96          final String password = form.password;
97          form.clearSecurityInfo();
98          try {
99              final HtmlResponse loginRedirect = fessLoginAssist.loginRedirect(new LocalUserCredential(username, password), op -> {}, () -> {
100                 activityHelper.login(getUserBean());
101                 userInfoHelper.deleteUserCodeFromCookie(request);
102                 return getHtmlResponse();
103             });
104             if (ComponentUtil.getFessConfig().isValidAdminPassword(password)) {
105                 return loginRedirect;
106             }
107             getSession().ifPresent(session -> session.setAttribute(INVALID_OLD_PASSWORD, password));
108             return asHtml(virtualHost(path_Login_NewpasswordJsp));
109         } catch (final LoginFailureException lfe) {
110             if (logger.isInfoEnabled()) {
111                 logger.info("Login failed for user: username={}, reason={}", username, lfe.getMessage());
112             }
113             activityHelper.loginFailure(OptionalThing.of(new LocalUserCredential(username, password)));
114             throwValidationError(messages -> messages.addErrorsLoginError(GLOBAL), () -> asIndexPage(form));
115         }
116         return redirect(getClass());
117     }
118 
119     /**
120      * Handles password change for the current user.
121      *
122      * @param form the password form containing new password and confirmation
123      * @return the HTML response after password change attempt
124      */
125     @Execute
126     public HtmlResponse changePassword(final PasswordForm form) {
127         final VaErrorHook toIndexPage = () -> {
128             form.clearSecurityInfo();
129             return getUserBean().map(u -> asHtml(virtualHost(path_Login_NewpasswordJsp)).useForm(PasswordForm.class))
130                     .orElseGet(() -> redirect(LoginAction.class));
131         };
132         validatePasswordForm(form, toIndexPage);
133         if (!getUserBean().isPresent()) {
134             logger.warn("User session not found during password change - potential session timeout or security issue");
135             return redirect(LoginAction.class);
136         }
137         final String username = getUserBean().get().getUserId();
138         try {
139             userService.changePassword(username, form.password);
140             saveInfo(messages -> messages.addSuccessChangedPassword(GLOBAL));
141         } catch (final Exception e) {
142             logger.warn("Failed to change password for user: username={}, error={}", username, e.getMessage(), e);
143             throwValidationError(messages -> messages.addErrorsFailedToChangePassword(GLOBAL), toIndexPage);
144         }
145         getSession().ifPresent(session -> session.removeAttribute(INVALID_OLD_PASSWORD));
146         return redirect(ProfileAction.class);
147     }
148 
149     private void validatePasswordForm(final PasswordForm form, final VaErrorHook validationErrorLambda) {
150         validate(form, messages -> {}, validationErrorLambda);
151 
152         if (!form.password.equals(form.confirmPassword)) {
153             throwValidationError(messages -> {
154                 messages.addErrorsInvalidConfirmPassword(GLOBAL);
155             }, validationErrorLambda);
156         }
157 
158         final String validationError = ComponentUtil.getSystemHelper().validatePassword(form.password);
159         if (StringUtil.isNotBlank(validationError)) {
160             throwValidationError(messages -> {
161                 addPasswordValidationError(messages, validationError);
162             }, validationErrorLambda);
163         }
164 
165         final String oldPassword =
166                 getSession().map(session -> (String) session.getAttribute(INVALID_OLD_PASSWORD)).orElse(StringUtil.EMPTY);
167         getUserBean().ifPresent(user -> {
168             final String userId = user.getUserId();
169             fessLoginAssist.findLoginUser(new LocalUserCredential(userId, oldPassword)).orElseGet(() -> {
170                 throwValidationError(messages -> {
171                     messages.addErrorsNoUserForChangingPassword(GLOBAL);
172                 }, validationErrorLambda);
173                 return null;
174             });
175         }).orElse(() -> {
176             throwValidationError(messages -> {
177                 messages.addErrorsLoginError(GLOBAL);
178             }, validationErrorLambda);
179         });
180     }
181 
182     /**
183      * Adds a password validation error message to the messages object based on the error key.
184      *
185      * @param messages the FessMessages object to add the error to
186      * @param errorKey the error key identifying the type of password validation error
187      */
188     protected void addPasswordValidationError(final FessMessages messages, final String errorKey) {
189         switch (errorKey) {
190         case "errors.password_length":
191             messages.addErrorsPasswordLength(GLOBAL, String.valueOf(ComponentUtil.getFessConfig().getPasswordMinLengthAsInteger()));
192             break;
193         case "errors.password_no_uppercase":
194             messages.addErrorsPasswordNoUppercase(GLOBAL);
195             break;
196         case "errors.password_no_lowercase":
197             messages.addErrorsPasswordNoLowercase(GLOBAL);
198             break;
199         case "errors.password_no_digit":
200             messages.addErrorsPasswordNoDigit(GLOBAL);
201             break;
202         case "errors.password_no_special_char":
203             messages.addErrorsPasswordNoSpecialChar(GLOBAL);
204             break;
205         case "errors.password_is_blacklisted":
206             messages.addErrorsPasswordIsBlacklisted(GLOBAL);
207             break;
208         default:
209             messages.addErrorsBlankPassword(GLOBAL);
210             break;
211         }
212     }
213 
214     private OptionalThing<HttpSession> getSession() {
215         final HttpSession session = request.getSession(false);
216         if (session != null) {
217             return OptionalEntity.of(session);
218         }
219         return OptionalEntity.empty();
220     }
221 }