diff --git a/src/main/java/org/codelibs/fess/app/web/base/login/SamlCredential.java b/src/main/java/org/codelibs/fess/app/web/base/login/SamlCredential.java index 22ef8bf10..83db28cc6 100644 --- a/src/main/java/org/codelibs/fess/app/web/base/login/SamlCredential.java +++ b/src/main/java/org/codelibs/fess/app/web/base/login/SamlCredential.java @@ -1,9 +1,25 @@ +/* + * Copyright 2012-2020 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ package org.codelibs.fess.app.web.base.login; import static org.codelibs.core.stream.StreamUtil.split; import static org.codelibs.core.stream.StreamUtil.stream; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -52,7 +68,8 @@ public class SamlCredential implements LoginCredential, FessCredential { } public SamlUser getUser() { - return new SamlUser(getUserId(), getDefaultGroupsAsArray(), getDefaultRolesAsArray()); + return new SamlUser(nameId, sessionIndex, nameIdFormat, nameidNameQualifier, nameidSPNameQualifier, getDefaultGroupsAsArray(), + getDefaultRolesAsArray()); } protected String[] getDefaultGroupsAsArray() { @@ -93,23 +110,36 @@ public class SamlCredential implements LoginCredential, FessCredential { private static final long serialVersionUID = 1L; - protected final String name; - protected String[] groups; protected String[] roles; protected String[] permissions; - protected SamlUser(final String name, final String[] groups, final String[] roles) { - this.name = name; + protected String nameId; + + protected String sessionIndex; + + protected String nameIdFormat; + + protected String nameidNameQualifier; + + protected String nameidSPNameQualifier; + + public SamlUser(final String nameId, final String sessionIndex, final String nameIdFormat, final String nameidNameQualifier, + final String nameidSPNameQualifier, final String[] groups, final String[] roles) { + this.nameId = nameId; + this.sessionIndex = sessionIndex; + this.nameIdFormat = nameIdFormat; + this.nameidNameQualifier = nameidNameQualifier; + this.nameidSPNameQualifier = nameidSPNameQualifier; this.groups = groups; this.roles = roles; } @Override public String getName() { - return name; + return nameId; } @Override @@ -127,7 +157,7 @@ public class SamlCredential implements LoginCredential, FessCredential { if (permissions == null) { final SystemHelper systemHelper = ComponentUtil.getSystemHelper(); final Set permissionSet = new HashSet<>(); - permissionSet.add(systemHelper.getSearchRoleByUser(name)); + permissionSet.add(systemHelper.getSearchRoleByUser(nameId)); stream(groups).of(stream -> stream.forEach(s -> permissionSet.add(systemHelper.getSearchRoleByGroup(s)))); stream(roles).of(stream -> stream.forEach(s -> permissionSet.add(systemHelper.getSearchRoleByRole(s)))); permissions = permissionSet.toArray(new String[permissionSet.size()]); @@ -135,5 +165,29 @@ public class SamlCredential implements LoginCredential, FessCredential { return permissions; } + public String getSessionIndex() { + return sessionIndex; + } + + public String getNameIdFormat() { + return nameIdFormat; + } + + public String getNameidNameQualifier() { + return nameidNameQualifier; + } + + public String getNameidSPNameQualifier() { + return nameidSPNameQualifier; + } + + @Override + public String toString() { + return "SamlUser [groups=" + Arrays.toString(groups) + ", roles=" + Arrays.toString(roles) + ", permissions=" + + Arrays.toString(permissions) + ", nameId=" + nameId + ", sessionIndex=" + sessionIndex + ", nameIdFormat=" + + nameIdFormat + ", nameidNameQualifier=" + nameidNameQualifier + ", nameidSPNameQualifier=" + nameidSPNameQualifier + + "]"; + } + } } diff --git a/src/main/java/org/codelibs/fess/app/web/logout/LogoutAction.java b/src/main/java/org/codelibs/fess/app/web/logout/LogoutAction.java index 794da3265..31de3b78e 100644 --- a/src/main/java/org/codelibs/fess/app/web/logout/LogoutAction.java +++ b/src/main/java/org/codelibs/fess/app/web/logout/LogoutAction.java @@ -15,9 +15,12 @@ */ package org.codelibs.fess.app.web.logout; +import org.codelibs.core.lang.StringUtil; import org.codelibs.fess.app.web.base.FessSearchAction; import org.codelibs.fess.app.web.login.LoginAction; import org.codelibs.fess.mylasta.action.FessUserBean; +import org.codelibs.fess.util.ComponentUtil; +import org.dbflute.optional.OptionalThing; import org.lastaflute.web.Execute; import org.lastaflute.web.response.HtmlResponse; @@ -41,11 +44,16 @@ public class LogoutAction extends FessSearchAction { @Execute public HtmlResponse index() { - getUserBean().map(FessUserBean::getUserId).orElse("-"); - activityHelper.logout(getUserBean()); + OptionalThing userBean = getUserBean(); + activityHelper.logout(userBean); + final String redirectUrl = userBean.map(user -> ComponentUtil.getSsoManager().logout(user)).orElse(null); fessLoginAssist.logout(); userInfoHelper.deleteUserCodeFromCookie(request); - return redirect(LoginAction.class); + if (StringUtil.isNotBlank(redirectUrl)) { + return HtmlResponse.fromRedirectPathAsIs(redirectUrl); + } else { + return redirect(LoginAction.class); + } } } \ No newline at end of file diff --git a/src/main/java/org/codelibs/fess/app/web/sso/SsoAction.java b/src/main/java/org/codelibs/fess/app/web/sso/SsoAction.java index f9298710f..197d879f8 100644 --- a/src/main/java/org/codelibs/fess/app/web/sso/SsoAction.java +++ b/src/main/java/org/codelibs/fess/app/web/sso/SsoAction.java @@ -21,9 +21,8 @@ import org.codelibs.fess.app.web.RootAction; import org.codelibs.fess.app.web.base.FessLoginAction; import org.codelibs.fess.app.web.base.login.ActionResponseCredential; import org.codelibs.fess.app.web.login.LoginAction; -import org.codelibs.fess.sso.SsoAuthenticator; import org.codelibs.fess.sso.SsoManager; -import org.codelibs.fess.sso.saml.SamlAuthenticator; +import org.codelibs.fess.sso.SsoResponseType; import org.codelibs.fess.util.ComponentUtil; import org.dbflute.optional.OptionalThing; import org.lastaflute.web.Execute; @@ -78,15 +77,22 @@ public class SsoAction extends FessLoginAction { } @Execute - public ActionResponse metadata(final String name) { - String key = name + "Authenticator"; - if (ComponentUtil.hasComponent(key)) { - throw responseManager.new400("Unknown request type: " + name); + public ActionResponse metadata() { + final SsoManager ssoManager = ComponentUtil.getSsoManager(); + final ActionResponse actionResponse = ssoManager.getResponse(SsoResponseType.METADATA); + if (actionResponse == null) { + throw responseManager.new400("Unsupported request type."); } - final SsoAuthenticator authenticator = ComponentUtil.getComponent(key); - if (authenticator instanceof SamlAuthenticator) { - return ((SamlAuthenticator) authenticator).getMetadataResponse(); + return actionResponse; + } + + @Execute + public ActionResponse logout() { + final SsoManager ssoManager = ComponentUtil.getSsoManager(); + final ActionResponse actionResponse = ssoManager.getResponse(SsoResponseType.LOGOUT); + if (actionResponse == null) { + throw responseManager.new400("Unsupported request type."); } - throw responseManager.new400("Unsupported request type: " + name); + return actionResponse; } } \ No newline at end of file diff --git a/src/main/java/org/codelibs/fess/sso/SsoAuthenticator.java b/src/main/java/org/codelibs/fess/sso/SsoAuthenticator.java index 9da524680..98c4adb7a 100644 --- a/src/main/java/org/codelibs/fess/sso/SsoAuthenticator.java +++ b/src/main/java/org/codelibs/fess/sso/SsoAuthenticator.java @@ -16,7 +16,9 @@ package org.codelibs.fess.sso; import org.codelibs.fess.app.web.base.login.FessLoginAssist.LoginCredentialResolver; +import org.codelibs.fess.mylasta.action.FessUserBean; import org.lastaflute.web.login.credential.LoginCredential; +import org.lastaflute.web.response.ActionResponse; public interface SsoAuthenticator { @@ -24,4 +26,8 @@ public interface SsoAuthenticator { void resolveCredential(LoginCredentialResolver resolver); + ActionResponse getResponse(SsoResponseType responseType); + + String logout(FessUserBean user); + } \ No newline at end of file diff --git a/src/main/java/org/codelibs/fess/sso/SsoManager.java b/src/main/java/org/codelibs/fess/sso/SsoManager.java index 9d793c495..1ab87bbc7 100644 --- a/src/main/java/org/codelibs/fess/sso/SsoManager.java +++ b/src/main/java/org/codelibs/fess/sso/SsoManager.java @@ -20,8 +20,10 @@ import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.codelibs.fess.mylasta.action.FessUserBean; import org.codelibs.fess.util.ComponentUtil; import org.lastaflute.web.login.credential.LoginCredential; +import org.lastaflute.web.response.ActionResponse; public class SsoManager { private static final Logger logger = LogManager.getLogger(SsoManager.class); @@ -42,8 +44,38 @@ public class SsoManager { public LoginCredential getLoginCredential() { if (available()) { - final SsoAuthenticator authenticator = ComponentUtil.getComponent(getSsoType() + "Authenticator"); - return authenticator.getLoginCredential(); + final SsoAuthenticator authenticator = getAuthenticator(); + if (authenticator != null) { + return authenticator.getLoginCredential(); + } + } + return null; + } + + public ActionResponse getResponse(final SsoResponseType responseType) { + if (available()) { + final SsoAuthenticator authenticator = getAuthenticator(); + if (authenticator != null) { + return authenticator.getResponse(responseType); + } + } + return null; + } + + public String logout(final FessUserBean user) { + if (available()) { + final SsoAuthenticator authenticator = getAuthenticator(); + if (authenticator != null) { + return authenticator.logout(user); + } + } + return null; + } + + protected SsoAuthenticator getAuthenticator() { + final String name = getSsoType() + "Authenticator"; + if (ComponentUtil.hasComponent(name)) { + return ComponentUtil.getComponent(name); } return null; } diff --git a/src/main/java/org/codelibs/fess/sso/SsoResponseType.java b/src/main/java/org/codelibs/fess/sso/SsoResponseType.java new file mode 100644 index 000000000..687213077 --- /dev/null +++ b/src/main/java/org/codelibs/fess/sso/SsoResponseType.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fess.sso; + +public enum SsoResponseType { + METADATA, LOGOUT; +} diff --git a/src/main/java/org/codelibs/fess/sso/aad/AzureAdAuthenticator.java b/src/main/java/org/codelibs/fess/sso/aad/AzureAdAuthenticator.java index b0c1885fb..e239599fb 100644 --- a/src/main/java/org/codelibs/fess/sso/aad/AzureAdAuthenticator.java +++ b/src/main/java/org/codelibs/fess/sso/aad/AzureAdAuthenticator.java @@ -54,11 +54,14 @@ import org.codelibs.fess.app.web.base.login.AzureAdCredential.AzureAdUser; import org.codelibs.fess.app.web.base.login.FessLoginAssist.LoginCredentialResolver; import org.codelibs.fess.crawler.Constants; import org.codelibs.fess.exception.SsoLoginException; +import org.codelibs.fess.mylasta.action.FessUserBean; import org.codelibs.fess.sso.SsoAuthenticator; +import org.codelibs.fess.sso.SsoResponseType; import org.codelibs.fess.util.ComponentUtil; import org.codelibs.fess.util.DocumentUtil; import org.dbflute.optional.OptionalEntity; import org.lastaflute.web.login.credential.LoginCredential; +import org.lastaflute.web.response.ActionResponse; import org.lastaflute.web.response.HtmlResponse; import org.lastaflute.web.util.LaRequestUtil; @@ -589,4 +592,14 @@ public class AzureAdAuthenticator implements SsoAuthenticator { public void setGroupCacheExpiry(final long groupCacheExpiry) { this.groupCacheExpiry = groupCacheExpiry; } + + @Override + public ActionResponse getResponse(final SsoResponseType responseType) { + return null; + } + + @Override + public String logout(final FessUserBean user) { + return null; + } } diff --git a/src/main/java/org/codelibs/fess/sso/oic/OpenIdConnectAuthenticator.java b/src/main/java/org/codelibs/fess/sso/oic/OpenIdConnectAuthenticator.java index 957a9f5c0..7f6580eaf 100644 --- a/src/main/java/org/codelibs/fess/sso/oic/OpenIdConnectAuthenticator.java +++ b/src/main/java/org/codelibs/fess/sso/oic/OpenIdConnectAuthenticator.java @@ -32,10 +32,13 @@ import org.codelibs.fess.app.web.base.login.ActionResponseCredential; import org.codelibs.fess.app.web.base.login.FessLoginAssist.LoginCredentialResolver; import org.codelibs.fess.app.web.base.login.OpenIdConnectCredential; import org.codelibs.fess.crawler.Constants; +import org.codelibs.fess.mylasta.action.FessUserBean; import org.codelibs.fess.sso.SsoAuthenticator; +import org.codelibs.fess.sso.SsoResponseType; import org.codelibs.fess.util.ComponentUtil; import org.dbflute.optional.OptionalEntity; import org.lastaflute.web.login.credential.LoginCredential; +import org.lastaflute.web.response.ActionResponse; import org.lastaflute.web.response.HtmlResponse; import org.lastaflute.web.util.LaRequestUtil; @@ -238,4 +241,14 @@ public class OpenIdConnectAuthenticator implements SsoAuthenticator { public void resolveCredential(final LoginCredentialResolver resolver) { resolver.resolve(OpenIdConnectCredential.class, credential -> OptionalEntity.of(credential.getUser())); } + + @Override + public ActionResponse getResponse(final SsoResponseType responseType) { + return null; + } + + @Override + public String logout(final FessUserBean user) { + return null; + } } diff --git a/src/main/java/org/codelibs/fess/sso/saml/SamlAuthenticator.java b/src/main/java/org/codelibs/fess/sso/saml/SamlAuthenticator.java index 6092839a3..61bead76b 100644 --- a/src/main/java/org/codelibs/fess/sso/saml/SamlAuthenticator.java +++ b/src/main/java/org/codelibs/fess/sso/saml/SamlAuthenticator.java @@ -1,3 +1,18 @@ +/* + * Copyright 2012-2020 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ package org.codelibs.fess.sso.saml; import java.io.OutputStreamWriter; @@ -19,9 +34,12 @@ import org.codelibs.core.net.UuidUtil; import org.codelibs.fess.app.web.base.login.ActionResponseCredential; import org.codelibs.fess.app.web.base.login.FessLoginAssist.LoginCredentialResolver; import org.codelibs.fess.app.web.base.login.SamlCredential; +import org.codelibs.fess.app.web.base.login.SamlCredential.SamlUser; import org.codelibs.fess.crawler.Constants; import org.codelibs.fess.exception.SsoLoginException; +import org.codelibs.fess.mylasta.action.FessUserBean; import org.codelibs.fess.sso.SsoAuthenticator; +import org.codelibs.fess.sso.SsoResponseType; import org.codelibs.fess.util.ComponentUtil; import org.dbflute.optional.OptionalEntity; import org.lastaflute.web.login.credential.LoginCredential; @@ -55,10 +73,10 @@ public class SamlAuthenticator implements SsoAuthenticator { defaultSettings = new HashMap<>(); defaultSettings.put("onelogin.saml2.strict", "true"); defaultSettings.put("onelogin.saml2.debug", "false"); - defaultSettings.put("onelogin.saml2.sp.entityid", "http://localhost:8080/sso/metadata/saml"); + defaultSettings.put("onelogin.saml2.sp.entityid", "http://localhost:8080/sso/metadata"); defaultSettings.put("onelogin.saml2.sp.assertion_consumer_service.url", "http://localhost:8080/sso/"); defaultSettings.put("onelogin.saml2.sp.assertion_consumer_service.binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"); - defaultSettings.put("onelogin.saml2.sp.single_logout_service.url", "http://localhost:8080/sso/logout/saml"); + defaultSettings.put("onelogin.saml2.sp.single_logout_service.url", "http://localhost:8080/sso/logout"); defaultSettings.put("onelogin.saml2.sp.single_logout_service.binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"); defaultSettings.put("onelogin.saml2.sp.nameidformat", "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"); defaultSettings.put("onelogin.saml2.sp.x509cert", ""); @@ -167,12 +185,48 @@ public class SamlAuthenticator implements SsoAuthenticator { } - public ActionResponse getMetadataResponse() { + @Override + public String logout(final FessUserBean user) { + if (user.getFessUser() instanceof SamlUser) { + return LaRequestUtil + .getOptionalRequest() + .map(request -> { + if (logger.isDebugEnabled()) { + logger.debug("Logging out with SAML Authenticator"); + } + final HttpServletResponse response = LaResponseUtil.getResponse(); + final SamlUser samlUser = (SamlUser) user.getFessUser(); + try { + final Auth auth = new Auth(getSettings(), request, response); + return auth.logout(null, samlUser.getName(), samlUser.getSessionIndex(), true, samlUser.getNameIdFormat(), + samlUser.getNameidNameQualifier(), samlUser.getNameidSPNameQualifier()); + } catch (final Exception e) { + logger.warn("Failed to logout from IdP: {}", samlUser, e); + } + return null; + }).orElse(null); + } + return null; + } + + @Override + public ActionResponse getResponse(final SsoResponseType responseType) { + switch (responseType) { + case METADATA: + return getMetadataResponse(); + case LOGOUT: + return getLogoutResponse(); + default: + return null; + } + } + + protected ActionResponse getMetadataResponse() { return LaRequestUtil .getOptionalRequest() .map(request -> { if (logger.isDebugEnabled()) { - logger.debug("Logging in with SAML Authenticator"); + logger.debug("Accessing metadata with SAML Authenticator"); } final HttpServletResponse response = LaResponseUtil.getResponse(); try { @@ -198,6 +252,31 @@ public class SamlAuthenticator implements SsoAuthenticator { }).orElseGet(() -> getStreamResponse("metadata", "text/html; charset=UTF-8", "Invalid state.")); } + protected ActionResponse getLogoutResponse() { + return LaRequestUtil + .getOptionalRequest() + .map(request -> { + if (logger.isDebugEnabled()) { + logger.debug("Logging out with SAML Authenticator"); + } + final HttpServletResponse response = LaResponseUtil.getResponse(); + try { + final Auth auth = new Auth(getSettings(), request, response); + auth.processSLO(); + final List errors = auth.getErrors(); + if (errors.isEmpty()) { + return getStreamResponse("logout", "text/html; charset=UTF-8", "Logged out"); + } else { + return getStreamResponse("logout", "text/html; charset=UTF-8", errors.stream().map(s -> "

" + s + "

") + .collect(Collectors.joining())); + } + } catch (final Exception e) { + logger.warn("Failed to process logout.", e); + return getStreamResponse("metadata", "text/html; charset=UTF-8", e.getMessage()); + } + }).orElseGet(() -> getStreamResponse("metadata", "text/html; charset=UTF-8", "Invalid state.")); + } + protected StreamResponse getStreamResponse(final String filename, final String contentType, final String content) { return new StreamResponse(filename).contentType(contentType).stream(out -> { try (final Writer writer = new OutputStreamWriter(out.stream(), Constants.UTF_8_CHARSET)) { @@ -205,5 +284,4 @@ public class SamlAuthenticator implements SsoAuthenticator { } }); } - } diff --git a/src/main/java/org/codelibs/fess/sso/spnego/SpnegoAuthenticator.java b/src/main/java/org/codelibs/fess/sso/spnego/SpnegoAuthenticator.java index a2f0d3284..dcf77b349 100644 --- a/src/main/java/org/codelibs/fess/sso/spnego/SpnegoAuthenticator.java +++ b/src/main/java/org/codelibs/fess/sso/spnego/SpnegoAuthenticator.java @@ -32,8 +32,10 @@ import org.codelibs.fess.app.web.base.login.ActionResponseCredential; import org.codelibs.fess.app.web.base.login.FessLoginAssist.LoginCredentialResolver; import org.codelibs.fess.app.web.base.login.SpnegoCredential; import org.codelibs.fess.exception.SsoLoginException; +import org.codelibs.fess.mylasta.action.FessUserBean; import org.codelibs.fess.mylasta.direction.FessConfig; import org.codelibs.fess.sso.SsoAuthenticator; +import org.codelibs.fess.sso.SsoResponseType; import org.codelibs.fess.util.ComponentUtil; import org.codelibs.spnego.SpnegoFilterConfig; import org.codelibs.spnego.SpnegoHttpFilter; @@ -42,6 +44,7 @@ import org.codelibs.spnego.SpnegoHttpServletResponse; import org.codelibs.spnego.SpnegoPrincipal; import org.dbflute.optional.OptionalEntity; import org.lastaflute.web.login.credential.LoginCredential; +import org.lastaflute.web.response.ActionResponse; import org.lastaflute.web.servlet.filter.RequestLoggingFilter; import org.lastaflute.web.util.LaRequestUtil; import org.lastaflute.web.util.LaResponseUtil; @@ -244,4 +247,15 @@ public class SpnegoAuthenticator implements SsoAuthenticator { return OptionalEntity.empty(); }); } + + @Override + public ActionResponse getResponse(final SsoResponseType responseType) { + return null; + } + + @Override + public String logout(final FessUserBean user) { + return null; + } + }