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.sso;
17  
18  import java.net.URLEncoder;
19  import java.util.ArrayList;
20  import java.util.List;
21  
22  import org.apache.logging.log4j.LogManager;
23  import org.apache.logging.log4j.Logger;
24  import org.codelibs.fess.Constants;
25  import org.codelibs.fess.app.web.RootAction;
26  import org.codelibs.fess.app.web.base.FessLoginAction;
27  import org.codelibs.fess.app.web.base.login.ActionResponseCredential;
28  import org.codelibs.fess.app.web.login.LoginAction;
29  import org.codelibs.fess.app.web.search.SearchAction;
30  import org.codelibs.fess.entity.RequestParameter;
31  import org.codelibs.fess.exception.SsoMessageException;
32  import org.codelibs.fess.sso.SsoManager;
33  import org.codelibs.fess.sso.SsoResponseType;
34  import org.codelibs.fess.util.ComponentUtil;
35  import org.dbflute.optional.OptionalThing;
36  import org.lastaflute.web.Execute;
37  import org.lastaflute.web.UrlChain;
38  import org.lastaflute.web.login.credential.LoginCredential;
39  import org.lastaflute.web.login.exception.LoginFailureException;
40  import org.lastaflute.web.response.ActionResponse;
41  import org.lastaflute.web.response.HtmlResponse;
42  
43  /**
44   * SSO (Single Sign-On) action controller.
45   *
46   * This action handles SSO authentication flows including login, logout, and metadata
47   * operations. It coordinates with the SsoManager to perform authentication using
48   * configured SSO providers and handles various authentication scenarios including
49   * successful login, authentication failures, and redirects.
50   */
51  public class SsoAction extends FessLoginAction {
52      // ===================================================================================
53      //                                                                            Constant
54      //
55      private static final Logger logger = LogManager.getLogger(SsoAction.class);
56  
57      /**
58       * Constructs a new SSO action.
59       */
60      public SsoAction() {
61          // do nothing
62      }
63  
64      // ===================================================================================
65      //                                                                       Login Execute
66      //                                                                      ==============
67  
68      /**
69       * Main SSO authentication endpoint.
70       *
71       * This method handles the primary SSO authentication flow. It checks if a user
72       * is already logged in, attempts SSO authentication, and handles various
73       * authentication scenarios including success, failure, and challenge responses.
74       *
75       * @return ActionResponse directing to the appropriate page based on authentication result
76       */
77      @Execute
78      public ActionResponse index() {
79          if (fessLoginAssist.getSavedUserBean().isPresent()) {
80              return redirectToSearchPage().orElseGet(() -> {
81                  if (logger.isDebugEnabled()) {
82                      logger.debug("User is already logged in, redirecting to root.");
83                  }
84                  return redirect(RootAction.class);
85              });
86          }
87          final SsoManager ssoManager = ComponentUtil.getSsoManager();
88          final LoginCredential loginCredential = ssoManager.getLoginCredential();
89          if (loginCredential == null) {
90              if (ssoManager.available()) {
91                  if (logger.isDebugEnabled()) {
92                      logger.debug("SSO is available but no user found.");
93                  }
94                  saveError(messages -> messages.addErrorsSsoLoginError(GLOBAL));
95              }
96              if (logger.isDebugEnabled()) {
97                  logger.debug("Redirecting to login page.");
98              }
99              return redirect(LoginAction.class);
100         }
101         if (loginCredential instanceof ActionResponseCredential) {
102             if (logger.isDebugEnabled()) {
103                 logger.debug("Login credential is an ActionResponseCredential, executing it.");
104             }
105             return ((ActionResponseCredential) loginCredential).execute();
106         }
107         try {
108             return fessLoginAssist.loginRedirect(loginCredential, op -> {}, () -> {
109                 if (logger.isDebugEnabled()) {
110                     logger.debug("Logging in user: {}", loginCredential);
111                 }
112                 activityHelper.login(getUserBean());
113                 userInfoHelper.deleteUserCodeFromCookie(request);
114                 return redirectToSearchPage().orElseGet(() -> {
115                     if (logger.isDebugEnabled()) {
116                         logger.debug("No search parameters found, redirecting to root.");
117                     }
118                     return getHtmlResponse();
119                 });
120             });
121         } catch (final LoginFailureException lfe) {
122             if (ssoManager.available()) {
123                 if (logger.isDebugEnabled()) {
124                     logger.debug("SSO is available but login failed.", lfe);
125                 }
126                 saveError(messages -> messages.addErrorsSsoLoginError(GLOBAL));
127             }
128             if (logger.isDebugEnabled()) {
129                 logger.debug("Redirecting to login page after failure.", lfe);
130             }
131             activityHelper.loginFailure(OptionalThing.of(loginCredential));
132             return redirect(LoginAction.class);
133         }
134     }
135 
136     /**
137      * SSO metadata endpoint.
138      *
139      * This method handles requests for SSO metadata, typically used by SAML or
140      * other SSO protocols that require metadata exchange. The actual metadata
141      * content is generated by the configured SSO authenticator.
142      *
143      * @return ActionResponse containing the SSO metadata or error page
144      */
145     @Execute
146     public ActionResponse metadata() {
147         final SsoManager ssoManager = ComponentUtil.getSsoManager();
148         try {
149             final ActionResponse actionResponse = ssoManager.getResponse(SsoResponseType.METADATA);
150             if (actionResponse == null) {
151                 throw responseManager.new400("Unsupported request type.");
152             }
153             return actionResponse;
154         } catch (final SsoMessageException e) {
155             if (e.getCause() == null) {
156                 if (logger.isDebugEnabled()) {
157                     logger.debug("Metadata response.", e);
158                 }
159                 saveInfo(e.getMessageCode());
160             } else {
161                 logger.warn("Failed to process metadata.", e);
162                 saveError(e.getMessageCode());
163             }
164             return redirect(LoginAction.class);
165         }
166     }
167 
168     /**
169      * Attempts to redirect to the search page with preserved search parameters.
170      *
171      * This method checks if there are saved search parameters from a previous
172      * session and redirects the user to the search page with those parameters
173      * restored. This provides a seamless user experience after authentication.
174      *
175      * @return Optional HtmlResponse containing the redirect to search page with parameters,
176      *         or empty if no search parameters were found
177      */
178     protected OptionalThing<HtmlResponse> redirectToSearchPage() {
179         final RequestParameter[] searchParameters = searchHelper.getSearchParameters();
180         if (searchParameters.length > 0) {
181             final List<String> paramList = new ArrayList<>();
182             for (final RequestParameter param : searchParameters) {
183                 for (final String value : param.getValues()) {
184                     paramList.add(param.getName());
185                     paramList.add(URLEncoder.encode(value, Constants.CHARSET_UTF_8));
186                 }
187             }
188             if (logger.isDebugEnabled()) {
189                 logger.debug("Redirecting to SearchAction with parameters: {}", paramList);
190             }
191             return OptionalThing.of(redirectWith(SearchAction.class, new UrlChain(this).params(paramList.toArray(n -> new Object[n]))));
192         }
193         return OptionalThing.empty();
194     }
195 
196     /**
197      * SSO logout endpoint.
198      *
199      * This method handles SSO logout requests, coordinating with the SSO provider
200      * to properly terminate the user's SSO session. It may involve redirecting
201      * to the SSO provider's logout endpoint or performing local logout operations.
202      *
203      * @return ActionResponse directing to the logout page or SSO provider logout endpoint
204      */
205     @Execute
206     public ActionResponse logout() {
207         final SsoManager ssoManager = ComponentUtil.getSsoManager();
208         try {
209             final ActionResponse actionResponse = ssoManager.getResponse(SsoResponseType.LOGOUT);
210             if (actionResponse == null) {
211                 throw responseManager.new400("Unsupported request type.");
212             }
213             return actionResponse;
214         } catch (final SsoMessageException e) {
215             if (e.getCause() == null) {
216                 if (logger.isDebugEnabled()) {
217                     logger.debug("Logout response.", e);
218                 }
219                 saveInfo(e.getMessageCode());
220             } else {
221                 logger.warn("Failed to log out.", e);
222                 saveError(e.getMessageCode());
223             }
224             return redirect(LoginAction.class);
225         }
226     }
227 }