View Javadoc
1   /*
2    * Copyright 2012-2021 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.sso.saml;
17  
18  import java.io.OutputStreamWriter;
19  import java.io.Writer;
20  import java.util.HashMap;
21  import java.util.List;
22  import java.util.Map;
23  import java.util.stream.Collectors;
24  
25  import javax.annotation.PostConstruct;
26  import javax.servlet.http.HttpServletResponse;
27  import javax.servlet.http.HttpSession;
28  
29  import org.apache.logging.log4j.LogManager;
30  import org.apache.logging.log4j.Logger;
31  import org.codelibs.core.lang.StringUtil;
32  import org.codelibs.core.misc.DynamicProperties;
33  import org.codelibs.core.net.UuidUtil;
34  import org.codelibs.fess.app.web.base.login.ActionResponseCredential;
35  import org.codelibs.fess.app.web.base.login.FessLoginAssist.LoginCredentialResolver;
36  import org.codelibs.fess.app.web.base.login.SamlCredential;
37  import org.codelibs.fess.app.web.base.login.SamlCredential.SamlUser;
38  import org.codelibs.fess.crawler.Constants;
39  import org.codelibs.fess.exception.SsoLoginException;
40  import org.codelibs.fess.exception.SsoMessageException;
41  import org.codelibs.fess.exception.SsoProcessException;
42  import org.codelibs.fess.mylasta.action.FessUserBean;
43  import org.codelibs.fess.sso.SsoAuthenticator;
44  import org.codelibs.fess.sso.SsoResponseType;
45  import org.codelibs.fess.util.ComponentUtil;
46  import org.dbflute.optional.OptionalEntity;
47  import org.lastaflute.core.message.UserMessages;
48  import org.lastaflute.web.login.credential.LoginCredential;
49  import org.lastaflute.web.response.ActionResponse;
50  import org.lastaflute.web.response.HtmlResponse;
51  import org.lastaflute.web.response.StreamResponse;
52  import org.lastaflute.web.util.LaRequestUtil;
53  import org.lastaflute.web.util.LaResponseUtil;
54  
55  import com.onelogin.saml2.Auth;
56  import com.onelogin.saml2.settings.Saml2Settings;
57  import com.onelogin.saml2.settings.SettingsBuilder;
58  
59  public class SamlAuthenticator implements SsoAuthenticator {
60  
61      private static final Logger logger = LogManager.getLogger(SamlAuthenticator.class);
62  
63      protected static final String SAML_PREFIX = "saml.";
64  
65      protected static final String SAML_STATE = "SAML_STATE";
66  
67      private Map<String, Object> defaultSettings;
68  
69      @PostConstruct
70      public void init() {
71          if (logger.isDebugEnabled()) {
72              logger.debug("Initialize {}", this.getClass().getSimpleName());
73          }
74          ComponentUtil.getSsoManager().register(this);
75  
76          defaultSettings = new HashMap<>();
77          defaultSettings.put("onelogin.saml2.strict", "true");
78          defaultSettings.put("onelogin.saml2.debug", "false");
79          defaultSettings.put("onelogin.saml2.sp.entityid", "http://localhost:8080/sso/metadata");
80          defaultSettings.put("onelogin.saml2.sp.assertion_consumer_service.url", "http://localhost:8080/sso/");
81          defaultSettings.put("onelogin.saml2.sp.assertion_consumer_service.binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST");
82          defaultSettings.put("onelogin.saml2.sp.single_logout_service.url", "http://localhost:8080/sso/logout");
83          defaultSettings.put("onelogin.saml2.sp.single_logout_service.binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect");
84          defaultSettings.put("onelogin.saml2.sp.nameidformat", "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress");
85          defaultSettings.put("onelogin.saml2.sp.x509cert", "");
86          defaultSettings.put("onelogin.saml2.sp.privatekey", "");
87          defaultSettings.put("onelogin.saml2.idp.single_sign_on_service.binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect");
88          defaultSettings.put("onelogin.saml2.idp.single_logout_service.response.url", "");
89          defaultSettings.put("onelogin.saml2.idp.single_logout_service.binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect");
90          defaultSettings.put("onelogin.saml2.security.nameid_encrypted", "false");
91          defaultSettings.put("onelogin.saml2.security.authnrequest_signed", "false");
92          defaultSettings.put("onelogin.saml2.security.logoutrequest_signed", "false");
93          defaultSettings.put("onelogin.saml2.security.logoutresponse_signed", "false");
94          defaultSettings.put("onelogin.saml2.security.want_messages_signed", "false");
95          defaultSettings.put("onelogin.saml2.security.want_assertions_signed", "false");
96          defaultSettings.put("onelogin.saml2.security.sign_metadata", "");
97          defaultSettings.put("onelogin.saml2.security.want_assertions_encrypted", "false");
98          defaultSettings.put("onelogin.saml2.security.want_nameid_encrypted", "false");
99          defaultSettings.put("onelogin.saml2.security.requested_authncontext", "urn:oasis:names:tc:SAML:2.0:ac:classes:Password");
100         defaultSettings.put("onelogin.saml2.security.onelogin.saml2.security.requested_authncontextcomparison", "exact");
101         defaultSettings.put("onelogin.saml2.security.want_xml_validation", "true");
102         defaultSettings.put("onelogin.saml2.security.signature_algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
103         defaultSettings.put("onelogin.saml2.organization.name", "CodeLibs");
104         defaultSettings.put("onelogin.saml2.organization.displayname", "Fess");
105         defaultSettings.put("onelogin.saml2.organization.url", "https://fess.codelibs.org/");
106         defaultSettings.put("onelogin.saml2.organization.lang", "");
107         defaultSettings.put("onelogin.saml2.contacts.technical.given_name", "Technical Guy");
108         defaultSettings.put("onelogin.saml2.contacts.technical.email_address", "technical@example.com");
109         defaultSettings.put("onelogin.saml2.contacts.support.given_name", "Support Guy");
110         defaultSettings.put("onelogin.saml2.contacts.support.email_address", "support@@example.com");
111     }
112 
113     protected Saml2Settings getSettings() {
114         final Map<String, Object> params = new HashMap<>(defaultSettings);
115         final DynamicProperties systemProperties = ComponentUtil.getSystemProperties();
116         systemProperties.entrySet().stream().forEach(e -> {
117             final String key = e.getKey().toString();
118             if (!key.startsWith(SAML_PREFIX)) {
119                 return;
120             }
121             params.put("onelogin.saml2." + key.substring(SAML_PREFIX.length()), e.getValue());
122         });
123         return new SettingsBuilder().fromValues(params).build();
124     }
125 
126     @Override
127     public LoginCredential getLoginCredential() {
128         return LaRequestUtil.getOptionalRequest().map(request -> {
129             if (logger.isDebugEnabled()) {
130                 logger.debug("Logging in with SAML Authenticator");
131             }
132 
133             final HttpServletResponse response = LaResponseUtil.getResponse();
134 
135             final HttpSession session = request.getSession(false);
136             if (session != null) {
137                 final String sesState = (String) session.getAttribute(SAML_STATE);
138                 if (StringUtil.isNotBlank(sesState)) {
139                     session.removeAttribute(SAML_STATE);
140                     try {
141                         final Auth auth = new Auth(getSettings(), request, response);
142                         auth.processResponse();
143 
144                         if (!auth.isAuthenticated()) {
145                             if (logger.isDebugEnabled()) {
146                                 logger.debug("Authentication is failed.");
147                             }
148                             return null;
149                         }
150 
151                         final List<String> errors = auth.getErrors();
152                         if (!errors.isEmpty()) {
153                             logger.warn("{}", errors.stream().collect(Collectors.joining(", ")));
154                             if (auth.isDebugActive() && StringUtil.isNotBlank(auth.getLastErrorReason())) {
155                                 logger.warn("Authentication Failure: {} - Reason: {}", errors.stream().collect(Collectors.joining(", ")),
156                                         auth.getLastErrorReason());
157                             } else {
158                                 logger.warn("Authentication Failure: {}", errors.stream().collect(Collectors.joining(", ")));
159                             }
160                             return null;
161                         }
162 
163                         return new SamlCredential(auth);
164                     } catch (final Exception e) {
165                         logger.warn("Authentication is failed.", e);
166                         return null;
167                     }
168                 }
169             }
170 
171             try {
172                 final Auth auth = new Auth(getSettings(), request, response);
173                 final String loginUrl = auth.login(null, false, false, true, true);
174                 request.getSession().setAttribute(SAML_STATE, UuidUtil.create());
175                 return new ActionResponseCredential(() -> HtmlResponse.fromRedirectPathAsIs(loginUrl));
176             } catch (final Exception e) {
177                 throw new SsoLoginException("Invalid SAML redirect URL.", e);
178             }
179 
180         }).orElseGet(() -> null);
181     }
182 
183     @Override
184     public void resolveCredential(final LoginCredentialResolver resolver) {
185         resolver.resolve(SamlCredential.class, credential -> OptionalEntity.of(credential.getUser()));
186 
187     }
188 
189     @Override
190     public String logout(final FessUserBean user) {
191         if (user.getFessUser() instanceof SamlUser) {
192             return LaRequestUtil.getOptionalRequest().map(request -> {
193                 if (logger.isDebugEnabled()) {
194                     logger.debug("Logging out with SAML Authenticator");
195                 }
196                 final HttpServletResponse response = LaResponseUtil.getResponse();
197                 final SamlUser samlUser = (SamlUser) user.getFessUser();
198                 try {
199                     final Auth auth = new Auth(getSettings(), request, response);
200                     return auth.logout(null, samlUser.getName(), samlUser.getSessionIndex(), true, samlUser.getNameIdFormat(),
201                             samlUser.getNameidNameQualifier(), samlUser.getNameidSPNameQualifier());
202                 } catch (final Exception e) {
203                     logger.warn("Failed to logout from IdP: {}", samlUser, e);
204                 }
205                 return null;
206             }).orElse(null);
207         }
208         return null;
209     }
210 
211     @Override
212     public ActionResponse getResponse(final SsoResponseType responseType) {
213         switch (responseType) {
214         case METADATA:
215             return getMetadataResponse();
216         case LOGOUT:
217             return getLogoutResponse();
218         default:
219             return null;
220         }
221     }
222 
223     protected ActionResponse getMetadataResponse() {
224         return LaRequestUtil.getOptionalRequest().map(request -> {
225             if (logger.isDebugEnabled()) {
226                 logger.debug("Accessing metadata with SAML Authenticator");
227             }
228             final HttpServletResponse response = LaResponseUtil.getResponse();
229             try {
230                 final Auth auth = new Auth(getSettings(), request, response);
231                 final Saml2Settings settings = auth.getSettings();
232                 settings.setSPValidationOnly(true);
233                 final String metadata = settings.getSPMetadata();
234                 final List<String> errors = Saml2Settings.validateMetadata(metadata);
235                 if (!errors.isEmpty()) {
236                     final String msg = errors.stream().collect(Collectors.joining(", "));
237                     throw new SsoMessageException(
238                             messages -> messages.addErrorsFailedToProcessSsoRequest(UserMessages.GLOBAL_PROPERTY_KEY, msg),
239                             "Failed to log out.", new SsoProcessException(msg));
240                 }
241                 return new StreamResponse("metadata").contentType("application/xhtml+xml").stream(out -> {
242                     try (final Writer writer = new OutputStreamWriter(out.stream(), Constants.UTF_8_CHARSET)) {
243                         writer.write(metadata);
244                     }
245                 });
246             } catch (final SsoMessageException e) {
247                 throw e;
248             } catch (final Exception e) {
249                 throw new SsoMessageException(
250                         messages -> messages.addErrorsFailedToProcessSsoRequest(UserMessages.GLOBAL_PROPERTY_KEY, e.getMessage()),
251                         "Failed to process metadata.", e);
252             }
253         }).orElseThrow(() -> new SsoMessageException(
254                 messages -> messages.addErrorsFailedToProcessSsoRequest(UserMessages.GLOBAL_PROPERTY_KEY, "Invalid state."),
255                 "Failed to process metadata.", new SsoProcessException("Invalid state.")));
256     }
257 
258     protected ActionResponse getLogoutResponse() {
259         LaRequestUtil.getOptionalRequest().map(request -> {
260             if (logger.isDebugEnabled()) {
261                 logger.debug("Logging out with SAML Authenticator");
262             }
263             final HttpServletResponse response = LaResponseUtil.getResponse();
264             try {
265                 final Auth auth = new Auth(getSettings(), request, response);
266                 auth.processSLO();
267                 final List<String> errors = auth.getErrors();
268                 if (errors.isEmpty()) {
269                     throw new SsoMessageException(messages -> messages.addSuccessSsoLogout(UserMessages.GLOBAL_PROPERTY_KEY), "Logged out");
270                 }
271                 final String msg = errors.stream().collect(Collectors.joining(", "));
272                 throw new SsoMessageException(
273                         messages -> messages.addErrorsFailedToProcessSsoRequest(UserMessages.GLOBAL_PROPERTY_KEY, msg),
274                         "Failed to log out.", new SsoProcessException(msg));
275             } catch (final SsoMessageException e) {
276                 throw e;
277             } catch (final Exception e) {
278                 throw new SsoMessageException(
279                         messages -> messages.addErrorsFailedToProcessSsoRequest(UserMessages.GLOBAL_PROPERTY_KEY, e.getMessage()),
280                         "Failed to log out.", e);
281             }
282         }).orElseThrow(() -> new SsoMessageException(
283                 messages -> messages.addErrorsFailedToProcessSsoRequest(UserMessages.GLOBAL_PROPERTY_KEY, "Invalid state."),
284                 "Failed to log out.", new SsoProcessException("Invalid state.")));
285         return null;
286     }
287 }