fix #2469 saml support

This commit is contained in:
Shinsuke Sugaya 2020-07-04 09:28:21 +09:00
parent cc3aeefa0b
commit 59a80ec2d5
6 changed files with 379 additions and 5 deletions

View file

@ -1305,6 +1305,11 @@
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<dependency>
<groupId>com.onelogin</groupId>
<artifactId>java-saml</artifactId>
<version>2.5.0</version>
</dependency>
<!-- suggest library -->
<dependency>

View file

@ -0,0 +1,139 @@
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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.codelibs.core.lang.StringUtil;
import org.codelibs.fess.entity.FessUser;
import org.codelibs.fess.helper.SystemHelper;
import org.codelibs.fess.mylasta.direction.FessConfig;
import org.codelibs.fess.util.ComponentUtil;
import org.lastaflute.web.login.credential.LoginCredential;
import com.onelogin.saml2.Auth;
public class SamlCredential implements LoginCredential, FessCredential {
private final Map<String, List<String>> attributes;
private final String nameId;
private final String nameIdFormat;
private final String sessionIndex;
private final String nameidNameQualifier;
private final String nameidSPNameQualifier;
public SamlCredential(final Auth auth) {
attributes = auth.getAttributes();
nameId = auth.getNameId();
nameIdFormat = auth.getNameIdFormat();
sessionIndex = auth.getSessionIndex();
nameidNameQualifier = auth.getNameIdNameQualifier();
nameidSPNameQualifier = auth.getNameIdSPNameQualifier();
}
@Override
public String toString() {
return "{" + getUserId() + "}";
}
@Override
public String getUserId() {
return nameId;
}
public SamlUser getUser() {
return new SamlUser(getUserId(), getDefaultGroupsAsArray(), getDefaultRolesAsArray());
}
protected String[] getDefaultGroupsAsArray() {
final List<String> list = new ArrayList<>();
final FessConfig fessConfig = ComponentUtil.getFessConfig();
final String key = fessConfig.getSystemProperty("saml.attribute.group.name", "memberOf");
if (StringUtil.isNotBlank(key)) {
final List<String> nameList = attributes.get(key);
if (nameList != null) {
list.addAll(nameList);
}
}
final String value = fessConfig.getSystemProperty("saml.default.groups");
if (StringUtil.isNotBlank(value)) {
split(value, ",").of(stream -> stream.forEach(list::add));
}
return list.stream().filter(StringUtil::isNotBlank).map(String::trim).toArray(n -> new String[n]);
}
protected String[] getDefaultRolesAsArray() {
final List<String> list = new ArrayList<>();
final FessConfig fessConfig = ComponentUtil.getFessConfig();
final String key = fessConfig.getSystemProperty("saml.attribute.role.name");
if (StringUtil.isNotBlank(key)) {
final List<String> nameList = attributes.get(key);
if (nameList != null) {
list.addAll(nameList);
}
}
final String value = fessConfig.getSystemProperty("saml.default.roles");
if (StringUtil.isNotBlank(value)) {
split(value, ",").of(stream -> stream.forEach(list::add));
}
return list.stream().filter(StringUtil::isNotBlank).map(String::trim).toArray(n -> new String[n]);
}
public static class SamlUser implements FessUser {
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;
this.groups = groups;
this.roles = roles;
}
@Override
public String getName() {
return name;
}
@Override
public String[] getRoleNames() {
return roles;
}
@Override
public String[] getGroupNames() {
return groups;
}
@Override
public String[] getPermissions() {
if (permissions == null) {
final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
final Set<String> permissionSet = new HashSet<>();
permissionSet.add(systemHelper.getSearchRoleByUser(name));
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()]);
}
return permissions;
}
}
}

View file

@ -21,7 +21,9 @@ 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.util.ComponentUtil;
import org.dbflute.optional.OptionalThing;
import org.lastaflute.web.Execute;
@ -74,4 +76,17 @@ public class SsoAction extends FessLoginAction {
return redirect(LoginAction.class);
}
}
@Execute
public ActionResponse metadata(final String name) {
String key = name + "Authenticator";
if (ComponentUtil.hasComponent(key)) {
throw responseManager.new400("Unknown request type: " + name);
}
final SsoAuthenticator authenticator = ComponentUtil.getComponent(key);
if (authenticator instanceof SamlAuthenticator) {
return ((SamlAuthenticator) authenticator).getMetadataResponse();
}
throw responseManager.new400("Unsupported request type: " + name);
}
}

View file

@ -0,0 +1,209 @@
package org.codelibs.fess.sso.saml;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codelibs.core.lang.StringUtil;
import org.codelibs.core.misc.DynamicProperties;
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.crawler.Constants;
import org.codelibs.fess.exception.SsoLoginException;
import org.codelibs.fess.sso.SsoAuthenticator;
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.response.StreamResponse;
import org.lastaflute.web.util.LaRequestUtil;
import org.lastaflute.web.util.LaResponseUtil;
import com.onelogin.saml2.Auth;
import com.onelogin.saml2.settings.Saml2Settings;
import com.onelogin.saml2.settings.SettingsBuilder;
public class SamlAuthenticator implements SsoAuthenticator {
private static final Logger logger = LogManager.getLogger(SamlAuthenticator.class);
protected static final String SAML_PREFIX = "saml.";
protected static final String SAML_STATE = "SAML_STATE";
private Map<String, Object> defaultSettings;
@PostConstruct
public void init() {
if (logger.isDebugEnabled()) {
logger.debug("Initialize {}", this.getClass().getSimpleName());
}
ComponentUtil.getSsoManager().register(this);
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.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.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", "");
defaultSettings.put("onelogin.saml2.sp.privatekey", "");
defaultSettings.put("onelogin.saml2.idp.single_sign_on_service.binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect");
defaultSettings.put("onelogin.saml2.idp.single_logout_service.response.url", "");
defaultSettings.put("onelogin.saml2.idp.single_logout_service.binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect");
defaultSettings.put("onelogin.saml2.security.nameid_encrypted", "false");
defaultSettings.put("onelogin.saml2.security.authnrequest_signed", "false");
defaultSettings.put("onelogin.saml2.security.logoutrequest_signed", "false");
defaultSettings.put("onelogin.saml2.security.logoutresponse_signed", "false");
defaultSettings.put("onelogin.saml2.security.want_messages_signed", "false");
defaultSettings.put("onelogin.saml2.security.want_assertions_signed", "false");
defaultSettings.put("onelogin.saml2.security.sign_metadata", "");
defaultSettings.put("onelogin.saml2.security.want_assertions_encrypted", "false");
defaultSettings.put("onelogin.saml2.security.want_nameid_encrypted", "false");
defaultSettings.put("onelogin.saml2.security.requested_authncontext", "urn:oasis:names:tc:SAML:2.0:ac:classes:Password");
defaultSettings.put("onelogin.saml2.security.onelogin.saml2.security.requested_authncontextcomparison", "exact");
defaultSettings.put("onelogin.saml2.security.want_xml_validation", "true");
defaultSettings.put("onelogin.saml2.security.signature_algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
defaultSettings.put("onelogin.saml2.organization.name", "CodeLibs");
defaultSettings.put("onelogin.saml2.organization.displayname", "Fess");
defaultSettings.put("onelogin.saml2.organization.url", "https://fess.codelibs.org/");
defaultSettings.put("onelogin.saml2.organization.lang", "");
defaultSettings.put("onelogin.saml2.contacts.technical.given_name", "Technical Guy");
defaultSettings.put("onelogin.saml2.contacts.technical.email_address", "technical@example.com");
defaultSettings.put("onelogin.saml2.contacts.support.given_name", "Support Guy");
defaultSettings.put("onelogin.saml2.contacts.support.email_address", "support@@example.com");
}
protected Saml2Settings getSettings() {
final Map<String, Object> params = new HashMap<>(defaultSettings);
final DynamicProperties systemProperties = ComponentUtil.getSystemProperties();
systemProperties.entrySet().stream().forEach(e -> {
final String key = e.getKey().toString();
if (!key.startsWith(SAML_PREFIX)) {
return;
}
params.put("onelogin.saml2." + key.substring(SAML_PREFIX.length()), e.getValue());
});
return new SettingsBuilder().fromValues(params).build();
}
@Override
public LoginCredential getLoginCredential() {
return LaRequestUtil
.getOptionalRequest()
.map(request -> {
if (logger.isDebugEnabled()) {
logger.debug("Logging in with SAML Authenticator");
}
final HttpServletResponse response = LaResponseUtil.getResponse();
final HttpSession session = request.getSession(false);
if (session != null) {
final String sesState = (String) session.getAttribute(SAML_STATE);
if (StringUtil.isNotBlank(sesState)) {
session.removeAttribute(SAML_STATE);
try {
final Auth auth = new Auth(getSettings(), request, response);
auth.processResponse();
if (!auth.isAuthenticated()) {
if (logger.isDebugEnabled()) {
logger.debug("Authentication is failed.");
}
return null;
}
final List<String> errors = auth.getErrors();
if (!errors.isEmpty()) {
logger.warn("{}", errors.stream().collect(Collectors.joining(", ")));
if (auth.isDebugActive() && StringUtil.isNotBlank(auth.getLastErrorReason())) {
logger.warn("Authentication Failure: {} - Reason: {}",
errors.stream().collect(Collectors.joining(", ")), auth.getLastErrorReason());
} else {
logger.warn("Authentication Failure: {}", errors.stream().collect(Collectors.joining(", ")));
}
return null;
}
return new SamlCredential(auth);
} catch (final Exception e) {
logger.warn("Authentication is failed.", e);
return null;
}
}
}
try {
final Auth auth = new Auth(getSettings(), request, response);
final String loginUrl = auth.login(null, false, false, true, true);
request.getSession().setAttribute(SAML_STATE, UuidUtil.create());
return new ActionResponseCredential(() -> HtmlResponse.fromRedirectPathAsIs(loginUrl));
} catch (final Exception e) {
throw new SsoLoginException("Invalid SAML redirect URL.", e);
}
}).orElseGet(() -> null);
}
@Override
public void resolveCredential(final LoginCredentialResolver resolver) {
resolver.resolve(SamlCredential.class, credential -> OptionalEntity.of(credential.getUser()));
}
public ActionResponse getMetadataResponse() {
return LaRequestUtil
.getOptionalRequest()
.map(request -> {
if (logger.isDebugEnabled()) {
logger.debug("Logging in with SAML Authenticator");
}
final HttpServletResponse response = LaResponseUtil.getResponse();
try {
final Auth auth = new Auth(getSettings(), request, response);
final Saml2Settings settings = auth.getSettings();
settings.setSPValidationOnly(true);
final String metadata = settings.getSPMetadata();
final List<String> errors = Saml2Settings.validateMetadata(metadata);
if (errors.isEmpty()) {
return new StreamResponse("metadata").contentType("application/xhtml+xml").stream(out -> {
try (final Writer writer = new OutputStreamWriter(out.stream(), Constants.UTF_8_CHARSET)) {
writer.write(metadata);
}
});
} else {
return getStreamResponse("metadata", "text/html; charset=UTF-8", errors.stream().map(s -> "<p>" + s + "</p>")
.collect(Collectors.joining()));
}
} catch (final Exception e) {
logger.warn("Failed to process metadata.", 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)) {
writer.write(content);
}
});
}
}

View file

@ -512,24 +512,28 @@ public final class ComponentUtil {
}
}
public static boolean hasComponent(final String componentKey) {
return SingletonLaContainerFactory.getContainer().hasComponentDef(componentKey);
}
public static boolean hasViewHelper() {
return SingletonLaContainerFactory.getContainer().hasComponentDef(VIEW_HELPER);
return hasComponent(VIEW_HELPER);
}
public static boolean hasQueryHelper() {
return SingletonLaContainerFactory.getContainer().hasComponentDef(QUERY_HELPER);
return hasComponent(QUERY_HELPER);
}
public static boolean hasPopularWordHelper() {
return SingletonLaContainerFactory.getContainer().hasComponentDef(POPULAR_WORD_HELPER);
return hasComponent(POPULAR_WORD_HELPER);
}
public static boolean hasRelatedQueryHelper() {
return SingletonLaContainerFactory.getContainer().hasComponentDef(RELATED_QUERY_HELPER);
return hasComponent(RELATED_QUERY_HELPER);
}
public static boolean hasIngestFactory() {
return SingletonLaContainerFactory.getContainer().hasComponentDef(INGEST_FACTORY);
return hasComponent(INGEST_FACTORY);
}
public static boolean available() {

View file

@ -6,6 +6,8 @@
</component>
<component name="oicAuthenticator" class="org.codelibs.fess.sso.oic.OpenIdConnectAuthenticator">
</component>
<component name="samlAuthenticator" class="org.codelibs.fess.sso.saml.SamlAuthenticator">
</component>
<component name="spnegoAuthenticator" class="org.codelibs.fess.sso.spnego.SpnegoAuthenticator">
</component>
</components>