1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 org.apache.logging.log4j.LogManager;
26 import org.apache.logging.log4j.Logger;
27 import org.codelibs.core.lang.StringUtil;
28 import org.codelibs.core.misc.DynamicProperties;
29 import org.codelibs.core.net.UuidUtil;
30 import org.codelibs.fess.app.web.base.login.ActionResponseCredential;
31 import org.codelibs.fess.app.web.base.login.FessLoginAssist.LoginCredentialResolver;
32 import org.codelibs.fess.app.web.base.login.SamlCredential;
33 import org.codelibs.fess.app.web.base.login.SamlCredential.SamlUser;
34 import org.codelibs.fess.crawler.Constants;
35 import org.codelibs.fess.exception.SsoLoginException;
36 import org.codelibs.fess.exception.SsoMessageException;
37 import org.codelibs.fess.exception.SsoProcessException;
38 import org.codelibs.fess.mylasta.action.FessUserBean;
39 import org.codelibs.fess.sso.SsoAuthenticator;
40 import org.codelibs.fess.sso.SsoResponseType;
41 import org.codelibs.fess.util.ComponentUtil;
42 import org.codelibs.saml2.Auth;
43 import org.codelibs.saml2.core.authn.AuthnRequestParams;
44 import org.codelibs.saml2.core.logout.LogoutRequestParams;
45 import org.codelibs.saml2.core.settings.Saml2Settings;
46 import org.codelibs.saml2.core.settings.SettingsBuilder;
47 import org.dbflute.optional.OptionalEntity;
48 import org.lastaflute.core.message.UserMessages;
49 import org.lastaflute.web.login.credential.LoginCredential;
50 import org.lastaflute.web.response.ActionResponse;
51 import org.lastaflute.web.response.HtmlResponse;
52 import org.lastaflute.web.response.StreamResponse;
53 import org.lastaflute.web.util.LaRequestUtil;
54 import org.lastaflute.web.util.LaResponseUtil;
55
56 import jakarta.annotation.PostConstruct;
57 import jakarta.servlet.http.HttpServletRequest;
58 import jakarta.servlet.http.HttpServletResponse;
59 import jakarta.servlet.http.HttpSession;
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136 public class SamlAuthenticator implements SsoAuthenticator {
137
138
139
140
141 public SamlAuthenticator() {
142 super();
143 }
144
145 private static final Logger logger = LogManager.getLogger(SamlAuthenticator.class);
146
147
148
149
150 protected static final String SAML_PREFIX = "saml.";
151
152
153
154
155 protected static final String SAML_STATE = "SAML_STATE";
156
157
158
159
160 protected static final String SAML_SP_BASE_URL = "saml.sp.base.url";
161
162 private Map<String, Object> defaultSettings;
163
164
165
166
167 @PostConstruct
168 public void init() {
169 if (logger.isDebugEnabled()) {
170 logger.debug("Initializing {}", this.getClass().getSimpleName());
171 }
172 ComponentUtil.getSsoManager().register(this);
173
174
175
176
177
178
179
180
181 defaultSettings = new HashMap<>();
182 defaultSettings.put("onelogin.saml2.strict", "true");
183 defaultSettings.put("onelogin.saml2.debug", "false");
184 defaultSettings.put("onelogin.saml2.sp.entityid", buildDefaultUrl("/sso/metadata"));
185 defaultSettings.put("onelogin.saml2.sp.assertion_consumer_service.url", buildDefaultUrl("/sso/"));
186 defaultSettings.put("onelogin.saml2.sp.assertion_consumer_service.binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST");
187 defaultSettings.put("onelogin.saml2.sp.single_logout_service.url", buildDefaultUrl("/sso/logout"));
188 defaultSettings.put("onelogin.saml2.sp.single_logout_service.binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect");
189 defaultSettings.put("onelogin.saml2.sp.nameidformat", "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress");
190 defaultSettings.put("onelogin.saml2.sp.x509cert", "");
191 defaultSettings.put("onelogin.saml2.sp.privatekey", "");
192 defaultSettings.put("onelogin.saml2.idp.single_sign_on_service.binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect");
193 defaultSettings.put("onelogin.saml2.idp.single_logout_service.response.url", "");
194 defaultSettings.put("onelogin.saml2.idp.single_logout_service.binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect");
195 defaultSettings.put("onelogin.saml2.security.nameid_encrypted", "false");
196 defaultSettings.put("onelogin.saml2.security.authnrequest_signed", "false");
197 defaultSettings.put("onelogin.saml2.security.logoutrequest_signed", "false");
198 defaultSettings.put("onelogin.saml2.security.logoutresponse_signed", "false");
199 defaultSettings.put("onelogin.saml2.security.want_messages_signed", "false");
200 defaultSettings.put("onelogin.saml2.security.want_assertions_signed", "false");
201 defaultSettings.put("onelogin.saml2.security.sign_metadata", "");
202 defaultSettings.put("onelogin.saml2.security.want_assertions_encrypted", "false");
203 defaultSettings.put("onelogin.saml2.security.want_nameid_encrypted", "false");
204 defaultSettings.put("onelogin.saml2.security.requested_authncontext", "urn:oasis:names:tc:SAML:2.0:ac:classes:Password");
205 defaultSettings.put("onelogin.saml2.security.onelogin.saml2.security.requested_authncontextcomparison", "exact");
206 defaultSettings.put("onelogin.saml2.security.want_xml_validation", "true");
207 defaultSettings.put("onelogin.saml2.security.signature_algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
208 defaultSettings.put("onelogin.saml2.organization.name", "CodeLibs");
209 defaultSettings.put("onelogin.saml2.organization.displayname", "Fess");
210 defaultSettings.put("onelogin.saml2.organization.url", "https://fess.codelibs.org/");
211 defaultSettings.put("onelogin.saml2.organization.lang", "");
212 defaultSettings.put("onelogin.saml2.contacts.technical.given_name", "Technical Guy");
213 defaultSettings.put("onelogin.saml2.contacts.technical.email_address", "technical@example.com");
214 defaultSettings.put("onelogin.saml2.contacts.support.given_name", "Support Guy");
215 defaultSettings.put("onelogin.saml2.contacts.support.email_address", "support@example.com");
216 }
217
218
219
220
221
222
223
224
225
226 protected String buildDefaultUrl(final String path) {
227 final DynamicProperties systemProperties = ComponentUtil.getSystemProperties();
228 String baseUrl = systemProperties.getProperty(SAML_SP_BASE_URL);
229 if (StringUtil.isBlank(baseUrl)) {
230 baseUrl = "http://localhost:8080";
231 }
232 if (baseUrl.endsWith("/")) {
233 baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
234 }
235 return baseUrl + path;
236 }
237
238
239
240
241
242 protected Saml2Settings getSettings() {
243 final Map<String, Object> params = new HashMap<>(defaultSettings);
244 final DynamicProperties systemProperties = ComponentUtil.getSystemProperties();
245 systemProperties.entrySet().stream().forEach(e -> {
246 final String key = e.getKey().toString();
247 if (!key.startsWith(SAML_PREFIX)) {
248 return;
249 }
250 params.put("onelogin.saml2." + key.substring(SAML_PREFIX.length()), e.getValue());
251 });
252 return new SettingsBuilder().fromValues(params).build();
253 }
254
255 @Override
256 public LoginCredential getLoginCredential() {
257 return LaRequestUtil.getOptionalRequest().map(request -> {
258 if (logger.isDebugEnabled()) {
259 logger.debug("Logging in with SAML Authenticator");
260 }
261
262 final HttpServletResponse response = LaResponseUtil.getResponse();
263
264 final HttpSession session = request.getSession(false);
265 if (session != null) {
266 final String sesState = (String) session.getAttribute(SAML_STATE);
267 if (StringUtil.isNotBlank(sesState)) {
268 session.removeAttribute(SAML_STATE);
269 try {
270 final Auth auth = new Auth(getSettings(), request, response);
271 auth.processResponse();
272
273 if (!auth.isAuthenticated()) {
274 if (logger.isDebugEnabled()) {
275 logger.debug("Authentication failed.");
276 }
277 return null;
278 }
279
280 final List<String> errors = auth.getErrors();
281 if (!errors.isEmpty()) {
282 logger.warn("{}", errors.stream().collect(Collectors.joining(", ")));
283 if (auth.isDebugActive() && StringUtil.isNotBlank(auth.getLastErrorReason())) {
284 logger.warn("Authentication Failure: {} - Reason: {}", errors.stream().collect(Collectors.joining(", ")),
285 auth.getLastErrorReason());
286 } else {
287 logger.warn("Authentication Failure: {}", errors.stream().collect(Collectors.joining(", ")));
288 }
289 return null;
290 }
291
292 return createLoginCredential(request, response, auth);
293 } catch (final Exception e) {
294 logger.warn("Authentication failed.", e);
295 return null;
296 }
297 }
298 }
299
300 try {
301 final Auth auth = new Auth(getSettings(), request, response);
302 final AuthnRequestParams authnRequestParams = new AuthnRequestParams(false, false, true);
303 final String loginUrl = auth.login(null, authnRequestParams, true);
304 request.getSession().setAttribute(SAML_STATE, UuidUtil.create());
305 return new ActionResponseCredential(() -> HtmlResponse.fromRedirectPathAsIs(loginUrl));
306 } catch (final Exception e) {
307 throw new SsoLoginException("Invalid SAML redirect URL.", e);
308 }
309
310 }).orElse(null);
311 }
312
313
314
315
316
317
318
319
320 protected LoginCredential createLoginCredential(final HttpServletRequest request, final HttpServletResponse response, final Auth auth) {
321 final SamlCredential samlCredential = new SamlCredential(auth);
322 if (logger.isDebugEnabled()) {
323 logger.debug("SamlCredential: {}", samlCredential);
324 }
325 return samlCredential;
326 }
327
328 @Override
329 public void resolveCredential(final LoginCredentialResolver resolver) {
330 resolver.resolve(SamlCredential.class, credential -> OptionalEntity.of(credential.getUser()));
331 }
332
333 @Override
334 public String logout(final FessUserBean user) {
335 if (user.getFessUser() instanceof SamlUser) {
336 return LaRequestUtil.getOptionalRequest().map(request -> {
337 if (logger.isDebugEnabled()) {
338 logger.debug("Logging out with SAML Authenticator");
339 }
340 final HttpServletResponse response = LaResponseUtil.getResponse();
341 final SamlUser samlUser = (SamlUser) user.getFessUser();
342 try {
343 final Saml2Settings settings = getSettings();
344 if (settings.getIdpSingleLogoutServiceUrl() == null) {
345 if (logger.isDebugEnabled()) {
346 logger.debug("IdP single logout service URL is not configured, skipping SLO for user: {}", samlUser);
347 }
348 return null;
349 }
350 final Auth auth = new Auth(settings, request, response);
351 final LogoutRequestParams logoutRequestParams = new LogoutRequestParams(samlUser.getSessionIndex(), samlUser.getName(),
352 samlUser.getNameIdFormat(), samlUser.getNameidNameQualifier(), samlUser.getNameidSPNameQualifier());
353 return auth.logout(null, logoutRequestParams, true);
354 } catch (final Exception e) {
355 logger.warn("Failed to logout from IdP: name={}", samlUser.getName(), e);
356 }
357 return null;
358 }).orElse(null);
359 }
360 return null;
361 }
362
363 @Override
364 public ActionResponse getResponse(final SsoResponseType responseType) {
365 return switch (responseType) {
366 case METADATA -> getMetadataResponse();
367 case LOGOUT -> getLogoutResponse();
368 default -> null;
369 };
370 }
371
372
373
374
375
376 protected ActionResponse getMetadataResponse() {
377 return LaRequestUtil.getOptionalRequest().map(request -> {
378 if (logger.isDebugEnabled()) {
379 logger.debug("Accessing metadata with SAML Authenticator");
380 }
381 final HttpServletResponse response = LaResponseUtil.getResponse();
382 try {
383 final Auth auth = new Auth(getSettings(), request, response);
384 final Saml2Settings settings = auth.getSettings();
385 settings.setSPValidationOnly(true);
386 final String metadata = settings.getSPMetadata();
387 final List<String> errors = Saml2Settings.validateMetadata(metadata);
388 if (!errors.isEmpty()) {
389 final String msg = errors.stream().collect(Collectors.joining(", "));
390 throw new SsoMessageException(
391 messages -> messages.addErrorsFailedToProcessSsoRequest(UserMessages.GLOBAL_PROPERTY_KEY, msg),
392 "Failed to log out.", new SsoProcessException(msg));
393 }
394 return new StreamResponse("metadata").contentType("application/xhtml+xml").stream(out -> {
395 try (final Writer writer = new OutputStreamWriter(out.stream(), Constants.UTF_8_CHARSET)) {
396 writer.write(metadata);
397 }
398 });
399 } catch (final SsoMessageException e) {
400 throw e;
401 } catch (final Exception e) {
402 throw new SsoMessageException(
403 messages -> messages.addErrorsFailedToProcessSsoRequest(UserMessages.GLOBAL_PROPERTY_KEY, e.getMessage()),
404 "Failed to process metadata.", e);
405 }
406 })
407 .orElseThrow(() -> new SsoMessageException(
408 messages -> messages.addErrorsFailedToProcessSsoRequest(UserMessages.GLOBAL_PROPERTY_KEY, "Invalid state."),
409 "Failed to process metadata.", new SsoProcessException("Invalid state.")));
410 }
411
412
413
414
415
416 protected ActionResponse getLogoutResponse() {
417 LaRequestUtil.getOptionalRequest().map(request -> {
418 if (logger.isDebugEnabled()) {
419 logger.debug("Logging out with SAML Authenticator");
420 }
421 final HttpServletResponse response = LaResponseUtil.getResponse();
422 try {
423 final Auth auth = new Auth(getSettings(), request, response);
424 auth.processSLO();
425 final List<String> errors = auth.getErrors();
426 if (errors.isEmpty()) {
427 throw new SsoMessageException(messages -> messages.addSuccessSsoLogout(UserMessages.GLOBAL_PROPERTY_KEY), "Logged out");
428 }
429 final String msg = errors.stream().collect(Collectors.joining(", "));
430 throw new SsoMessageException(
431 messages -> messages.addErrorsFailedToProcessSsoRequest(UserMessages.GLOBAL_PROPERTY_KEY, msg),
432 "Failed to log out.", new SsoProcessException(msg));
433 } catch (final SsoMessageException e) {
434 throw e;
435 } catch (final Exception e) {
436 throw new SsoMessageException(
437 messages -> messages.addErrorsFailedToProcessSsoRequest(UserMessages.GLOBAL_PROPERTY_KEY, e.getMessage()),
438 "Failed to log out.", e);
439 }
440 })
441 .orElseThrow(() -> new SsoMessageException(
442 messages -> messages.addErrorsFailedToProcessSsoRequest(UserMessages.GLOBAL_PROPERTY_KEY, "Invalid state."),
443 "Failed to log out.", new SsoProcessException("Invalid state.")));
444 return null;
445 }
446 }