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 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 }