fix #2469 saml support
This commit is contained in:
parent
cc3aeefa0b
commit
59a80ec2d5
6 changed files with 379 additions and 5 deletions
5
pom.xml
5
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
209
src/main/java/org/codelibs/fess/sso/saml/SamlAuthenticator.java
Normal file
209
src/main/java/org/codelibs/fess/sso/saml/SamlAuthenticator.java
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue