Re-introduce force route option

This commit is contained in:
AFaust 2021-12-14 18:46:25 +01:00
parent a521dd87de
commit b02eaaa896
17 changed files with 294 additions and 55 deletions

View File

@ -11,7 +11,7 @@ Configuration of adapter properties in the Share-tier `share-config-custom.xml`
```xml ```xml
<config evaluator="string-compare" condition="Keycloak"> <config evaluator="string-compare" condition="Keycloak">
<keycloak-adapter-config> <keycloak-adapter-config>
<proxy-url></proxy-url> <forced-route-url></forced-route-url>
<auth-server-url>http://localhost:8180/auth</auth-server-url> <auth-server-url>http://localhost:8180/auth</auth-server-url>
<realm>alfresco</realm> <realm>alfresco</realm>
<resource>alfresco-share</resource> <resource>alfresco-share</resource>
@ -30,7 +30,8 @@ Note: This listing does not include the common property key prefix `keycloak.ada
| Property | Default Value | Description | | Property | Default Value | Description |
| --- | ---: | --- | | --- | ---: | --- |
| `auth-server-url` | `http://localhost:8180/auth` | Publically resolvable base URL to the Keycloak server to be used in redirect URLs and remote calls | | `auth-server-url` | `http://localhost:8180/auth` | Publically resolvable base URL to the Keycloak server to be used in redirect URLs and remote calls |
| `proxy-url` | | Alternative base URL for the Keycloak server (excluding path) to be used for calls from Alfresco to Keycloak - useful e.g. in scenarios where the regular `auth-server-url` can not be resolved or round-trips via a public gateway / proxy should be avoided | | `forced-route-url` | | Alternative base URL for the Keycloak server (excluding path) to be used for calls from Alfresco to Keycloak - useful e.g. in scenarios where the regular `auth-server-url` can not be resolved or round-trips via a public gateway / proxy should be avoided |
| `proxy-url` | | URL for proxy server to use for calls from Alfresco to Keycloak |
| `realm` | `alfresco` | Technical name of the Keycloak realm | | `realm` | `alfresco` | Technical name of the Keycloak realm |
| `realm-public-key` | | Fixed public key of the realm (PEM string) - if not set, the public key(s) will be dynamically loaded and automatically refreshed after a configurable amount of times between JSON Web Key Store requests | | `realm-public-key` | | Fixed public key of the realm (PEM string) - if not set, the public key(s) will be dynamically loaded and automatically refreshed after a configurable amount of times between JSON Web Key Store requests |
| `resource` | `alfresco` / `alfresco-share` | Technical name of the client set up in the realm | | `resource` | `alfresco` / `alfresco-share` | Technical name of the client set up in the realm |

View File

@ -22,6 +22,7 @@ The configuration options for the `keycloak-adapter-config` sub-element are [doc
| `enable-sso-filter` | `true` | Flag determining whether the SSO authentication handling logic is enabled - only if this is enabled (and `external-auth` configured for the main `alfresco` remote connector) will any of the functionality of the Share addon work. | | `enable-sso-filter` | `true` | Flag determining whether the SSO authentication handling logic is enabled - only if this is enabled (and `external-auth` configured for the main `alfresco` remote connector) will any of the functionality of the Share addon work. |
| `enhance-login-form` | `true` | Flag determining whether an additional "Log in via SSO" button is to be included in the Share login form | | `enhance-login-form` | `true` | Flag determining whether an additional "Log in via SSO" button is to be included in the Share login form |
| `force-keycloak-sso` | `false` | Flag determining whether SSO authentication should be forced, meaning users are automatically redirected for authentication to Keycloak and the login form is only accessible by using a direct URL access bypass | | `force-keycloak-sso` | `false` | Flag determining whether SSO authentication should be forced, meaning users are automatically redirected for authentication to Keycloak and the login form is only accessible by using a direct URL access bypass |
| `remember-keycloak-sso` | `false` | Flag determining whether SSO authentication should be remembered, meaning users are automatically redirected for authentication to Keycloak if they last logged in via Keycloak, when `force-keycloak-sso` is not enabled (if enabled, this will set an additional cookie in the client's browser) |
| `body-buffer-limit` | `10485760` | Size limit for request bodies that can be cached / stored if a request needs to be redirected to Keycloak for SSO authentication - requests larger than this limit will fail and require that the client first authenticate in a simple request, and use either authentication tickets or HTTP session cookies to perform the payload request re-using the established authentication | | `body-buffer-limit` | `10485760` | Size limit for request bodies that can be cached / stored if a request needs to be redirected to Keycloak for SSO authentication - requests larger than this limit will fail and require that the client first authenticate in a simple request, and use either authentication tickets or HTTP session cookies to perform the payload request re-using the established authentication |
| `session-mapper-limit` | `10000` | Size limit (in number of sessions) of the in-memory mapper of HTTP and SSO session IDs in order to allow back-channel logout requests to be properly handled. As HTTP sessions are not replicated in default Alfresco Share, the session mapper only handles local sessions for an individual Share node. | | `session-mapper-limit` | `10000` | Size limit (in number of sessions) of the in-memory mapper of HTTP and SSO session IDs in order to allow back-channel logout requests to be properly handled. As HTTP sessions are not replicated in default Alfresco Share, the session mapper only handles local sessions for an individual Share node. |
| `ignore-default-filter` | `true` | Flag determining whether the default SSO filter should be ignored / skipped when `enable-sso-filter` is enabled, in order to avoid functionality conflicts, e.g. via redundant handling of `Authorization` HTTP headers. | | `ignore-default-filter` | `true` | Flag determining whether the default SSO filter should be ignored / skipped when `enable-sso-filter` is enabled, in order to avoid functionality conflicts, e.g. via redundant handling of `Authorization` HTTP headers. |

View File

@ -98,7 +98,7 @@ The following core configuration properties can be set (more extensive list in t
| `...groupFilter.containedInGroup.property.groupPaths` | | Comma-separated list of group paths (e.g. `/Group A/Group B,/Group A/Group C`) to use in filtering which groups are synchronised to Alfresco (by default - configured separately - any match qualifies, and transitive containment is considered) | | `...groupFilter.containedInGroup.property.groupPaths` | | Comma-separated list of group paths (e.g. `/Group A/Group B,/Group A/Group C`) to use in filtering which groups are synchronised to Alfresco (by default - configured separately - any match qualifies, and transitive containment is considered) |
| `...groupFilter.containedInGroup.property.groupIds` | | Comma-separated list of group IDs to use in filtering which groups are synchronised to Alfresco (by default - configured separately - any match qualifies, and transitive containment is considered) | | `...groupFilter.containedInGroup.property.groupIds` | | Comma-separated list of group IDs to use in filtering which groups are synchronised to Alfresco (by default - configured separately - any match qualifies, and transitive containment is considered) |
| `keycloak.adapter.auth-server-url` | `http://localhost:8180/auth` | Publically resolvable base URL to the Keycloak server to be used in redirect URLs and remote calls | | `keycloak.adapter.auth-server-url` | `http://localhost:8180/auth` | Publically resolvable base URL to the Keycloak server to be used in redirect URLs and remote calls |
| `...proxy-url` | | Alternative base URL for the Keycloak server (excluding path) to be used for calls from Alfresco to Keycloak - useful e.g. in scenarios where the regular `auth-server-url` can not be resolved by the Alfresco Repository host or round-trips via a public gateway / proxy should be avoided | | `...forced-route-url` | | Alternative base URL for the Keycloak server (excluding path) to be used for calls from Alfresco to Keycloak - useful e.g. in scenarios where the regular `auth-server-url` can not be resolved by the Alfresco Repository host or round-trips via a public gateway / proxy should be avoided |
| `...realm` | `alfresco` | Technical name of the Keycloak realm | | `...realm` | `alfresco` | Technical name of the Keycloak realm |
| `...resource` | `alfresco` | Technical name of the client set up for the Alfresco Repository in the realm | | `...resource` | `alfresco` | Technical name of the client set up for the Alfresco Repository in the realm |
| `...credentials.secret` | | Shared secret for validation of authorisation codes / access tokens | | `...credentials.secret` | | Shared secret for validation of authorisation codes / access tokens |
@ -118,7 +118,7 @@ The following showcases an example configuration block:
<perform-token-exchange>true</perform-token-exchange> <perform-token-exchange>true</perform-token-exchange>
</keycloak-auth-config> </keycloak-auth-config>
<keycloak-adapter-config> <keycloak-adapter-config>
<proxy-url></proxy-url> <forced-route-url></forced-route-url>
<auth-server-url>http://localhost:8180/auth</auth-server-url> <auth-server-url>http://localhost:8180/auth</auth-server-url>
<realm>alfresco</realm> <realm>alfresco</realm>
<resource>alfresco-share</resource> <resource>alfresco-share</resource>

View File

@ -16,6 +16,7 @@ keycloak.authentication.silentRemoteUserValidationFailure=true
keycloak.authentication.bodyBufferLimit=10485760 keycloak.authentication.bodyBufferLimit=10485760
keycloak.adapter.auth-server-url=http://localhost:8180/auth keycloak.adapter.auth-server-url=http://localhost:8180/auth
keycloak.adapter.forced-route-url=
keycloak.adapter.proxy-url= keycloak.adapter.proxy-url=
keycloak.adapter.realm=alfresco keycloak.adapter.realm=alfresco
keycloak.adapter.resource=alfresco keycloak.adapter.resource=alfresco

View File

@ -0,0 +1,59 @@
/*
* Copyright 2019 - 2021 Acosix GmbH
*
* 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 de.acosix.alfresco.keycloak.repo.spring;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import org.keycloak.representations.adapters.config.AdapterConfig;
/**
* Minorly extended configuration for Java based adapters
*
* @author Axel Faust
*/
@JsonPropertyOrder({ "realm", "realm-public-key", "auth-server-url", "ssl-required", "resource", "public-client", "credentials",
"use-resource-role-mappings", "enable-cors", "cors-max-age", "cors-allowed-methods", "cors-exposed-headers", "expose-token",
"bearer-only", "autodetect-bearer-only", "connection-pool-size", "socket-timeout-millis", "connection-ttl-millis",
"connection-timeout-millis", "allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password", "client-keystore",
"client-keystore-password", "client-key-password", "always-refresh-token", "register-node-at-startup", "register-node-period",
"token-store", "adapter-state-cookie-path", "principal-attribute", "proxy-url", "forced-route-url",
"turn-off-change-session-id-on-login", "token-minimum-time-to-live", "min-time-between-jwks-requests", "public-key-cache-ttl",
"policy-enforcer", "ignore-oauth-query-parameter", "verify-token-audience" })
public class ExtendedAdapterConfig extends AdapterConfig
{
@JsonProperty("forced-route-url")
protected String forcedRouteUrl;
/**
* @return the forcedRouteUrl
*/
public String getForcedRouteUrl()
{
return this.forcedRouteUrl;
}
/**
* @param forcedRouteUrl
* the forcedRouteUrl to set
*/
public void setForcedRouteUrl(final String forcedRouteUrl)
{
this.forcedRouteUrl = forcedRouteUrl;
}
}

View File

@ -33,7 +33,6 @@ import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.util.PropertyCheck; import org.alfresco.util.PropertyCheck;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.FactoryBean;
@ -44,7 +43,7 @@ import org.springframework.util.PropertyPlaceholderHelper;
/** /**
* @author Axel Faust * @author Axel Faust
*/ */
public class KeycloakAdapterConfigBeanFactory implements FactoryBean<AdapterConfig>, InitializingBean public class KeycloakAdapterConfigBeanFactory implements FactoryBean<ExtendedAdapterConfig>, InitializingBean
{ {
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAdapterConfigBeanFactory.class); private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAdapterConfigBeanFactory.class);
@ -74,7 +73,7 @@ public class KeycloakAdapterConfigBeanFactory implements FactoryBean<AdapterConf
primitiveWrapperTypeMap.put(primitiveTypes[i], wrapperTypes[i]); primitiveWrapperTypeMap.put(primitiveTypes[i], wrapperTypes[i]);
} }
Class<?> cls = AdapterConfig.class; Class<?> cls = ExtendedAdapterConfig.class;
while (cls != null && !Object.class.equals(cls)) while (cls != null && !Object.class.equals(cls))
{ {
final Field[] fields = cls.getDeclaredFields(); final Field[] fields = cls.getDeclaredFields();
@ -152,7 +151,7 @@ public class KeycloakAdapterConfigBeanFactory implements FactoryBean<AdapterConf
/** /**
* @param propertiesSource * @param propertiesSource
* the propertiesSource to set * the propertiesSource to set
*/ */
public void setPropertiesSource(final Properties propertiesSource) public void setPropertiesSource(final Properties propertiesSource)
{ {
@ -161,7 +160,7 @@ public class KeycloakAdapterConfigBeanFactory implements FactoryBean<AdapterConf
/** /**
* @param configPropertyPrefix * @param configPropertyPrefix
* the configPropertyPrefix to set * the configPropertyPrefix to set
*/ */
public void setConfigPropertyPrefix(final String configPropertyPrefix) public void setConfigPropertyPrefix(final String configPropertyPrefix)
{ {
@ -170,7 +169,7 @@ public class KeycloakAdapterConfigBeanFactory implements FactoryBean<AdapterConf
/** /**
* @param placeholderPrefix * @param placeholderPrefix
* the placeholderPrefix to set * the placeholderPrefix to set
*/ */
public void setPlaceholderPrefix(final String placeholderPrefix) public void setPlaceholderPrefix(final String placeholderPrefix)
{ {
@ -179,7 +178,7 @@ public class KeycloakAdapterConfigBeanFactory implements FactoryBean<AdapterConf
/** /**
* @param placeholderSuffix * @param placeholderSuffix
* the placeholderSuffix to set * the placeholderSuffix to set
*/ */
public void setPlaceholderSuffix(final String placeholderSuffix) public void setPlaceholderSuffix(final String placeholderSuffix)
{ {
@ -188,7 +187,7 @@ public class KeycloakAdapterConfigBeanFactory implements FactoryBean<AdapterConf
/** /**
* @param valueSeparator * @param valueSeparator
* the valueSeparator to set * the valueSeparator to set
*/ */
public void setValueSeparator(final String valueSeparator) public void setValueSeparator(final String valueSeparator)
{ {
@ -199,9 +198,9 @@ public class KeycloakAdapterConfigBeanFactory implements FactoryBean<AdapterConf
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public AdapterConfig getObject() throws Exception public ExtendedAdapterConfig getObject() throws Exception
{ {
final AdapterConfig adapterConfig = new AdapterConfig(); final ExtendedAdapterConfig adapterConfig = new ExtendedAdapterConfig();
CONFIG_NAMES.forEach(configFieldName -> { CONFIG_NAMES.forEach(configFieldName -> {
final Class<?> valueType = VALUE_TYPE_BY_CONFIG_NAME.get(configFieldName); final Class<?> valueType = VALUE_TYPE_BY_CONFIG_NAME.get(configFieldName);
@ -248,7 +247,7 @@ public class KeycloakAdapterConfigBeanFactory implements FactoryBean<AdapterConf
@Override @Override
public Class<?> getObjectType() public Class<?> getObjectType()
{ {
return AdapterConfig.class; return ExtendedAdapterConfig.class;
} }
protected Object loadConfigValue(final String configFieldName, final Class<?> valueType) protected Object loadConfigValue(final String configFieldName, final Class<?> valueType)

View File

@ -15,18 +15,27 @@
*/ */
package de.acosix.alfresco.keycloak.repo.spring; package de.acosix.alfresco.keycloak.repo.spring;
import java.net.InetAddress;
import org.alfresco.httpclient.HttpClientFactory.NonBlockingHttpParamsFactory; import org.alfresco.httpclient.HttpClientFactory.NonBlockingHttpParamsFactory;
import org.alfresco.util.PropertyCheck; import org.alfresco.util.PropertyCheck;
import org.apache.commons.httpclient.params.DefaultHttpParams; import org.apache.commons.httpclient.params.DefaultHttpParams;
import org.apache.http.HttpHost;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.params.ConnRouteParams;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.params.HttpParams;
import org.keycloak.adapters.HttpClientBuilder;
import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
/** /**
* @author Axel Faust * @author Axel Faust
*/ */
@SuppressWarnings("deprecation")
public class KeycloakDeploymentBeanFactory implements FactoryBean<KeycloakDeployment>, InitializingBean public class KeycloakDeploymentBeanFactory implements FactoryBean<KeycloakDeployment>, InitializingBean
{ {
@ -36,7 +45,7 @@ public class KeycloakDeploymentBeanFactory implements FactoryBean<KeycloakDeploy
DefaultHttpParams.setHttpParamsFactory(new NonBlockingHttpParamsFactory()); DefaultHttpParams.setHttpParamsFactory(new NonBlockingHttpParamsFactory());
} }
protected AdapterConfig adapterConfig; protected ExtendedAdapterConfig adapterConfig;
/** /**
* *
@ -52,7 +61,7 @@ public class KeycloakDeploymentBeanFactory implements FactoryBean<KeycloakDeploy
* @param adapterConfig * @param adapterConfig
* the adapterConfig to set * the adapterConfig to set
*/ */
public void setAdapterConfig(final AdapterConfig adapterConfig) public void setAdapterConfig(final ExtendedAdapterConfig adapterConfig)
{ {
this.adapterConfig = adapterConfig; this.adapterConfig = adapterConfig;
} }
@ -63,7 +72,12 @@ public class KeycloakDeploymentBeanFactory implements FactoryBean<KeycloakDeploy
@Override @Override
public KeycloakDeployment getObject() throws Exception public KeycloakDeployment getObject() throws Exception
{ {
return KeycloakDeploymentBuilder.build(this.adapterConfig); final KeycloakDeployment keycloakDeployment = KeycloakDeploymentBuilder.build(this.adapterConfig);
final HttpClientBuilder httpClientBuilder = new HttpClientBuilder();
final HttpClient client = httpClientBuilder.build(this.adapterConfig);
this.configureForcedRouteIfNecessary(client, this.adapterConfig.getForcedRouteUrl());
keycloakDeployment.setClient(client);
return keycloakDeployment;
} }
/** /**
@ -86,4 +100,27 @@ public class KeycloakDeploymentBeanFactory implements FactoryBean<KeycloakDeploy
{ {
return KeycloakDeployment.class; return KeycloakDeployment.class;
} }
protected void configureForcedRouteIfNecessary(final HttpClient client, final String forcedRoute)
{
if (forcedRoute != null && !forcedRoute.isEmpty())
{
final HttpHost forcedRouteHost = HttpHost.create(forcedRoute);
final HttpParams params = client.getParams();
final InetAddress local = ConnRouteParams.getLocalAddress(params);
final HttpHost defaultProxy = ConnRouteParams.getDefaultProxy(params);
final boolean secure = forcedRouteHost.getSchemeName().equalsIgnoreCase("https");
HttpRoute route;
if (defaultProxy == null)
{
route = new HttpRoute(forcedRouteHost, local, secure);
}
else
{
route = new HttpRoute(forcedRouteHost, local, defaultProxy, secure);
}
params.setParameter(ConnRoutePNames.FORCED_ROUTE, route);
}
}
} }

View File

@ -1,5 +1,7 @@
package de.acosix.alfresco.keycloak.repo.token; package de.acosix.alfresco.keycloak.repo.token;
import com.fasterxml.jackson.core.JsonParseException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
@ -327,6 +329,10 @@ public class AccessTokenClient
{ {
return JsonSerialization.readValue(is, responseCls); return JsonSerialization.readValue(is, responseCls);
} }
catch (final JsonParseException jpe)
{
throw new AccessTokenException("Failed to parse access token response", jpe);
}
finally finally
{ {
try try

View File

@ -31,6 +31,7 @@
<enable-sso-filter>true</enable-sso-filter> <enable-sso-filter>true</enable-sso-filter>
<enhance-login-form>true</enhance-login-form> <enhance-login-form>true</enhance-login-form>
<force-keycloak-sso>false</force-keycloak-sso> <force-keycloak-sso>false</force-keycloak-sso>
<remember-keycloak-sso>false</remember-keycloak-sso>
<body-buffer-limit>10485760</body-buffer-limit> <body-buffer-limit>10485760</body-buffer-limit>
<session-mapper-limit>10000</session-mapper-limit> <session-mapper-limit>10000</session-mapper-limit>
<ignore-default-filter>true</ignore-default-filter> <ignore-default-filter>true</ignore-default-filter>

View File

@ -0,0 +1,59 @@
/*
* Copyright 2019 - 2021 Acosix GmbH
*
* 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 de.acosix.alfresco.keycloak.share.config;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import org.keycloak.representations.adapters.config.AdapterConfig;
/**
* Minorly extended configuration for Java based adapters
*
* @author Axel Faust
*/
@JsonPropertyOrder({ "realm", "realm-public-key", "auth-server-url", "ssl-required", "resource", "public-client", "credentials",
"use-resource-role-mappings", "enable-cors", "cors-max-age", "cors-allowed-methods", "cors-exposed-headers", "expose-token",
"bearer-only", "autodetect-bearer-only", "connection-pool-size", "socket-timeout-millis", "connection-ttl-millis",
"connection-timeout-millis", "allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password", "client-keystore",
"client-keystore-password", "client-key-password", "always-refresh-token", "register-node-at-startup", "register-node-period",
"token-store", "adapter-state-cookie-path", "principal-attribute", "proxy-url", "forced-route-url",
"turn-off-change-session-id-on-login", "token-minimum-time-to-live", "min-time-between-jwks-requests", "public-key-cache-ttl",
"policy-enforcer", "ignore-oauth-query-parameter", "verify-token-audience" })
public class ExtendedAdapterConfig extends AdapterConfig
{
@JsonProperty("forced-route-url")
protected String forcedRouteUrl;
/**
* @return the forcedRouteUrl
*/
public String getForcedRouteUrl()
{
return this.forcedRouteUrl;
}
/**
* @param forcedRouteUrl
* the forcedRouteUrl to set
*/
public void setForcedRouteUrl(final String forcedRouteUrl)
{
this.forcedRouteUrl = forcedRouteUrl;
}
}

View File

@ -34,13 +34,11 @@ import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.util.EqualsHelper; import org.alfresco.util.EqualsHelper;
import org.alfresco.util.ParameterCheck; import org.alfresco.util.ParameterCheck;
import org.alfresco.util.PropertyCheck; import org.alfresco.util.PropertyCheck;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.extensions.config.ConfigElement; import org.springframework.extensions.config.ConfigElement;
import de.acosix.alfresco.utility.share.config.BaseCustomConfigElement; import de.acosix.alfresco.utility.share.config.BaseCustomConfigElement;
import de.acosix.alfresco.utility.share.config.ConfigValueHolder;
/** /**
* @author Axel Faust * @author Axel Faust
@ -79,7 +77,7 @@ public class KeycloakAdapterConfigElement extends BaseCustomConfigElement
primitiveWrapperTypeMap.put(primitiveTypes[i], wrapperTypes[i]); primitiveWrapperTypeMap.put(primitiveTypes[i], wrapperTypes[i]);
} }
Class<?> cls = AdapterConfig.class; Class<?> cls = ExtendedAdapterConfig.class;
while (cls != null && !Object.class.equals(cls)) while (cls != null && !Object.class.equals(cls))
{ {
final Field[] fields = cls.getDeclaredFields(); final Field[] fields = cls.getDeclaredFields();
@ -152,8 +150,7 @@ public class KeycloakAdapterConfigElement extends BaseCustomConfigElement
public boolean isFieldSupported(final String fieldName) public boolean isFieldSupported(final String fieldName)
{ {
ParameterCheck.mandatoryString("fieldName", fieldName); ParameterCheck.mandatoryString("fieldName", fieldName);
final boolean supported = CONFIG_NAMES.contains(fieldName); return CONFIG_NAMES.contains(fieldName);
return supported;
} }
/** /**
@ -170,22 +167,21 @@ public class KeycloakAdapterConfigElement extends BaseCustomConfigElement
{ {
throw new IllegalArgumentException(fieldName + " is not a supported field"); throw new IllegalArgumentException(fieldName + " is not a supported field");
} }
final Class<?> valueType = VALUE_TYPE_BY_CONFIG_NAME.get(fieldName); return VALUE_TYPE_BY_CONFIG_NAME.get(fieldName);
return valueType;
} }
/** /**
* Retrieves the configured value for a specific field. Default values inherent in the {@link AdapterConfig Keycloak classes} are not * Retrieves the configured value for a specific field. Default values inherent in the {@link ExtendedAdapterConfig Keycloak classes}
* are not
* considered by this operation. * considered by this operation.
* *
* @param fieldName * @param fieldName
* the name of the field for which to retrieve the value * the name of the field for which to retrieve the value
* @return the currently configured value for the field, or {@code null} if no value has been configured * @return the currently configured value for the field, or {@code null} if no value has been configured
*/ */
public Object getFieldValue(final String fieldName) public Object getFieldValue(final String fieldName)
{ {
final Object value = this.configValueByField.get(fieldName); return this.configValueByField.get(fieldName);
return value;
} }
/** /**
@ -252,8 +248,7 @@ public class KeycloakAdapterConfigElement extends BaseCustomConfigElement
{ {
throw new IllegalArgumentException(fieldName + " is not a supported field"); throw new IllegalArgumentException(fieldName + " is not a supported field");
} }
final boolean unset = this.markedAsUnset.contains(fieldName); return this.markedAsUnset.contains(fieldName);
return unset;
} }
/** /**
@ -261,9 +256,9 @@ public class KeycloakAdapterConfigElement extends BaseCustomConfigElement
* *
* @return the adapter configuration instance * @return the adapter configuration instance
*/ */
public AdapterConfig buildAdapterConfiguration() public ExtendedAdapterConfig buildAdapterConfiguration()
{ {
final AdapterConfig adapterConfig = new AdapterConfig(); final ExtendedAdapterConfig adapterConfig = new ExtendedAdapterConfig();
try try
{ {

View File

@ -37,6 +37,8 @@ public class KeycloakAuthenticationConfigElement extends BaseCustomConfigElement
protected final ConfigValueHolder<Boolean> forceKeycloakSso = new ConfigValueHolder<>(); protected final ConfigValueHolder<Boolean> forceKeycloakSso = new ConfigValueHolder<>();
protected final ConfigValueHolder<Boolean> rememberKeycloakSso = new ConfigValueHolder<>();
protected final ConfigValueHolder<Integer> bodyBufferLimit = new ConfigValueHolder<>(); protected final ConfigValueHolder<Integer> bodyBufferLimit = new ConfigValueHolder<>();
protected final ConfigValueHolder<Integer> sessionMapperLimit = new ConfigValueHolder<>(); protected final ConfigValueHolder<Integer> sessionMapperLimit = new ConfigValueHolder<>();
@ -57,7 +59,7 @@ public class KeycloakAuthenticationConfigElement extends BaseCustomConfigElement
/** /**
* @param enhanceLoginForm * @param enhanceLoginForm
* the enhanceLoginForm to set * the enhanceLoginForm to set
*/ */
public void setEnhanceLoginForm(final Boolean enhanceLoginForm) public void setEnhanceLoginForm(final Boolean enhanceLoginForm)
{ {
@ -74,7 +76,7 @@ public class KeycloakAuthenticationConfigElement extends BaseCustomConfigElement
/** /**
* @param enableSsoFilter * @param enableSsoFilter
* the enableSsoFilter to set * the enableSsoFilter to set
*/ */
public void setEnableSsoFilter(final Boolean enableSsoFilter) public void setEnableSsoFilter(final Boolean enableSsoFilter)
{ {
@ -91,7 +93,7 @@ public class KeycloakAuthenticationConfigElement extends BaseCustomConfigElement
/** /**
* @param forceKeycloakSso * @param forceKeycloakSso
* the forceKeycloakSso to set * the forceKeycloakSso to set
*/ */
public void setForceKeycloakSso(final Boolean forceKeycloakSso) public void setForceKeycloakSso(final Boolean forceKeycloakSso)
{ {
@ -106,9 +108,26 @@ public class KeycloakAuthenticationConfigElement extends BaseCustomConfigElement
return this.forceKeycloakSso.getValue(); return this.forceKeycloakSso.getValue();
} }
/**
* @param rememberKeycloakSso
* the rememberKeycloakSso to set
*/
public void setRememberKeycloakSso(final Boolean rememberKeycloakSso)
{
this.rememberKeycloakSso.setValue(rememberKeycloakSso);
}
/**
* @return the rememberKeycloakSso
*/
public Boolean getRememberKeycloakSso()
{
return this.rememberKeycloakSso.getValue();
}
/** /**
* @param bodyBufferLimit * @param bodyBufferLimit
* the bodyBufferLimit to set * the bodyBufferLimit to set
*/ */
public void setBodyBufferLimit(final Integer bodyBufferLimit) public void setBodyBufferLimit(final Integer bodyBufferLimit)
{ {
@ -125,7 +144,7 @@ public class KeycloakAuthenticationConfigElement extends BaseCustomConfigElement
/** /**
* @param sessionMapperLimit * @param sessionMapperLimit
* the sessionMapperLimit to set * the sessionMapperLimit to set
*/ */
public void setSessionMapperLimit(final Integer sessionMapperLimit) public void setSessionMapperLimit(final Integer sessionMapperLimit)
{ {
@ -142,7 +161,7 @@ public class KeycloakAuthenticationConfigElement extends BaseCustomConfigElement
/** /**
* @param ignoreDefaultFilter * @param ignoreDefaultFilter
* the ignoreDefaultFilter to set * the ignoreDefaultFilter to set
*/ */
public void setIgnoreDefaultFilter(final Boolean ignoreDefaultFilter) public void setIgnoreDefaultFilter(final Boolean ignoreDefaultFilter)
{ {
@ -159,7 +178,7 @@ public class KeycloakAuthenticationConfigElement extends BaseCustomConfigElement
/** /**
* @param performTokenExchange * @param performTokenExchange
* the performTokenExchange to set * the performTokenExchange to set
*/ */
public void setPerformTokenExchange(final Boolean performTokenExchange) public void setPerformTokenExchange(final Boolean performTokenExchange)
{ {
@ -176,7 +195,7 @@ public class KeycloakAuthenticationConfigElement extends BaseCustomConfigElement
/** /**
* @param alfrescoResourceName * @param alfrescoResourceName
* the alfrescoResourceName to set * the alfrescoResourceName to set
*/ */
public void setAlfrescoResourceName(final String alfrescoResourceName) public void setAlfrescoResourceName(final String alfrescoResourceName)
{ {
@ -236,6 +255,17 @@ public class KeycloakAuthenticationConfigElement extends BaseCustomConfigElement
: this.getForceKeycloakSso()); : this.getForceKeycloakSso());
} }
if (otherConfigElement.rememberKeycloakSso.isUnset())
{
combined.rememberKeycloakSso.unset();
}
else
{
combined.setRememberKeycloakSso(
otherConfigElement.getRememberKeycloakSso() != null ? otherConfigElement.getRememberKeycloakSso()
: this.getRememberKeycloakSso());
}
if (otherConfigElement.bodyBufferLimit.isUnset()) if (otherConfigElement.bodyBufferLimit.isUnset())
{ {
combined.bodyBufferLimit.unset(); combined.bodyBufferLimit.unset();
@ -309,6 +339,9 @@ public class KeycloakAuthenticationConfigElement extends BaseCustomConfigElement
builder.append("forceKeycloakSso="); builder.append("forceKeycloakSso=");
builder.append(this.forceKeycloakSso); builder.append(this.forceKeycloakSso);
builder.append(", "); builder.append(", ");
builder.append("rememberKeycloakSso=");
builder.append(this.rememberKeycloakSso);
builder.append(", ");
builder.append("bodyBufferLimit="); builder.append("bodyBufferLimit=");
builder.append(this.bodyBufferLimit); builder.append(this.bodyBufferLimit);
builder.append(", "); builder.append(", ");

View File

@ -58,6 +58,13 @@ public class KeycloakAuthenticationConfigElementReader implements ConfigElementR
configElement.setForceKeycloakSso(value.isEmpty() ? null : Boolean.valueOf(value)); configElement.setForceKeycloakSso(value.isEmpty() ? null : Boolean.valueOf(value));
} }
final Element rememberKeycloakSso = element.element("remember-keycloak-sso");
if (rememberKeycloakSso != null)
{
final String value = rememberKeycloakSso.getTextTrim();
configElement.setRememberKeycloakSso(value.isEmpty() ? null : Boolean.valueOf(value));
}
final Element bodyBufferLimit = element.element("body-buffer-limit"); final Element bodyBufferLimit = element.element("body-buffer-limit");
if (bodyBufferLimit != null) if (bodyBufferLimit != null)
{ {

View File

@ -22,6 +22,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.net.InetAddress;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -48,12 +49,17 @@ import org.alfresco.util.EqualsHelper;
import org.alfresco.util.PropertyCheck; import org.alfresco.util.PropertyCheck;
import org.alfresco.web.site.servlet.SSOAuthenticationFilter; import org.alfresco.web.site.servlet.SSOAuthenticationFilter;
import org.apache.http.HttpEntity; import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient; import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.params.ConnRouteParams;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.HttpParams;
import org.apache.http.util.EntityUtils; import org.apache.http.util.EntityUtils;
import org.keycloak.KeycloakSecurityContext; import org.keycloak.KeycloakSecurityContext;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
@ -84,7 +90,6 @@ import org.keycloak.common.util.Time;
import org.keycloak.constants.ServiceUrlConstants; import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -115,6 +120,7 @@ import org.springframework.extensions.webscripts.servlet.DependencyInjectedFilte
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import de.acosix.alfresco.keycloak.share.config.ExtendedAdapterConfig;
import de.acosix.alfresco.keycloak.share.config.KeycloakAdapterConfigElement; import de.acosix.alfresco.keycloak.share.config.KeycloakAdapterConfigElement;
import de.acosix.alfresco.keycloak.share.config.KeycloakAuthenticationConfigElement; import de.acosix.alfresco.keycloak.share.config.KeycloakAuthenticationConfigElement;
import de.acosix.alfresco.keycloak.share.config.KeycloakConfigConstants; import de.acosix.alfresco.keycloak.share.config.KeycloakConfigConstants;
@ -127,6 +133,7 @@ import de.acosix.alfresco.keycloak.share.util.RefreshableAccessTokenHolder;
* *
* @author Axel Faust * @author Axel Faust
*/ */
@SuppressWarnings("deprecation")
public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, InitializingBean, ApplicationContextAware public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, InitializingBean, ApplicationContextAware
{ {
@ -227,6 +234,8 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
protected boolean forceSso = false; protected boolean forceSso = false;
protected boolean rememberSso = false;
protected boolean ignoreDefaultFilter = false; protected boolean ignoreDefaultFilter = false;
protected KeycloakDeployment keycloakDeployment; protected KeycloakDeployment keycloakDeployment;
@ -316,6 +325,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
this.filterEnabled = Boolean.TRUE.equals(keycloakAuthConfig.getEnableSsoFilter()); this.filterEnabled = Boolean.TRUE.equals(keycloakAuthConfig.getEnableSsoFilter());
this.loginFormEnhancementEnabled = Boolean.TRUE.equals(keycloakAuthConfig.getEnhanceLoginForm()); this.loginFormEnhancementEnabled = Boolean.TRUE.equals(keycloakAuthConfig.getEnhanceLoginForm());
this.forceSso = Boolean.TRUE.equals(keycloakAuthConfig.getForceKeycloakSso()); this.forceSso = Boolean.TRUE.equals(keycloakAuthConfig.getForceKeycloakSso());
this.rememberSso = Boolean.TRUE.equals(keycloakAuthConfig.getRememberKeycloakSso());
this.ignoreDefaultFilter = Boolean.TRUE.equals(keycloakAuthConfig.getIgnoreDefaultFilter()); this.ignoreDefaultFilter = Boolean.TRUE.equals(keycloakAuthConfig.getIgnoreDefaultFilter());
} }
else else
@ -503,8 +513,15 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
protected void initFromAdapterConfig(final KeycloakAdapterConfigElement keycloakAdapterConfig) protected void initFromAdapterConfig(final KeycloakAdapterConfigElement keycloakAdapterConfig)
{ {
final AdapterConfig adapterConfiguration = keycloakAdapterConfig.buildAdapterConfiguration(); final ExtendedAdapterConfig adapterConfiguration = keycloakAdapterConfig.buildAdapterConfiguration();
this.keycloakDeployment = KeycloakDeploymentBuilder.build(adapterConfiguration); this.keycloakDeployment = KeycloakDeploymentBuilder.build(adapterConfiguration);
final String forcedRouteUrl = adapterConfiguration.getForcedRouteUrl();
if (forcedRouteUrl != null && !forcedRouteUrl.isEmpty())
{
final HttpClient client = this.keycloakDeployment.getClient();
this.configureForcedRouteIfNecessary(client, forcedRouteUrl);
this.keycloakDeployment.setClient(client);
}
this.deploymentContext = new AdapterDeploymentContext(this.keycloakDeployment); this.deploymentContext = new AdapterDeploymentContext(this.keycloakDeployment);
} }
@ -531,12 +548,15 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
tokenStore.logout(); tokenStore.logout();
final Cookie keycloakCookie = new Cookie(KEYCLOAK_AUTHENTICATED_COOKIE, "false"); if (this.rememberSso)
keycloakCookie.setPath(context.getContextPath()); {
keycloakCookie.setMaxAge(0); final Cookie keycloakCookie = new Cookie(KEYCLOAK_AUTHENTICATED_COOKIE, "false");
keycloakCookie.setHttpOnly(true); keycloakCookie.setPath(context.getContextPath());
keycloakCookie.setSecure(req.isSecure()); keycloakCookie.setMaxAge(0);
res.addCookie(keycloakCookie); keycloakCookie.setHttpOnly(true);
keycloakCookie.setSecure(req.isSecure());
res.addCookie(keycloakCookie);
}
chain.doFilter(req, res); chain.doFilter(req, res);
} }
@ -1010,7 +1030,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
{ {
final boolean hasKeycloakCookie = this.hasKeycloakCookie(req); final boolean hasKeycloakCookie = this.hasKeycloakCookie(req);
if (!hasKeycloakCookie) if (!hasKeycloakCookie && this.rememberSso)
{ {
final Cookie keycloakCookie = new Cookie(KEYCLOAK_AUTHENTICATED_COOKIE, "true"); final Cookie keycloakCookie = new Cookie(KEYCLOAK_AUTHENTICATED_COOKIE, "true");
keycloakCookie.setPath(req.getServletContext().getContextPath()); keycloakCookie.setPath(req.getServletContext().getContextPath());
@ -1025,7 +1045,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
{ {
final Cookie[] cookies = req.getCookies(); final Cookie[] cookies = req.getCookies();
boolean hasKeycloakCookie = false; boolean hasKeycloakCookie = false;
if (cookies != null) if (cookies != null && this.rememberSso)
{ {
for (final Cookie cookie : cookies) for (final Cookie cookie : cookies)
{ {
@ -1827,4 +1847,24 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
} }
return sslPort; return sslPort;
} }
protected void configureForcedRouteIfNecessary(final HttpClient client, final String forcedRoute)
{
final HttpHost forcedRouteHost = HttpHost.create(forcedRoute);
final HttpParams params = client.getParams();
final InetAddress local = ConnRouteParams.getLocalAddress(params);
final HttpHost defaultProxy = ConnRouteParams.getDefaultProxy(params);
final boolean secure = forcedRouteHost.getSchemeName().equalsIgnoreCase("https");
HttpRoute route;
if (defaultProxy == null)
{
route = new HttpRoute(forcedRouteHost, local, secure);
}
else
{
route = new HttpRoute(forcedRouteHost, local, defaultProxy, secure);
}
params.setParameter(ConnRoutePNames.FORCED_ROUTE, route);
}
} }

View File

@ -25,7 +25,7 @@ keycloak.adapter.credentials.provider=secret
keycloak.adapter.credentials.secret=6f70a28f-98cd-41ca-8f2f-368a8797d708 keycloak.adapter.credentials.secret=6f70a28f-98cd-41ca-8f2f-368a8797d708
# localhost in auth-server-url won't work for direct access in a Docker deployment # localhost in auth-server-url won't work for direct access in a Docker deployment
keycloak.adapter.proxy-url=http://keycloak:8080 keycloak.adapter.forced-route-url=http://keycloak:8080
keycloak.roles.requiredClientScopes=alfresco-role-service keycloak.roles.requiredClientScopes=alfresco-role-service

View File

@ -75,7 +75,7 @@
<perform-token-exchange>true</perform-token-exchange> <perform-token-exchange>true</perform-token-exchange>
</keycloak-auth-config> </keycloak-auth-config>
<keycloak-adapter-config> <keycloak-adapter-config>
<proxy-url>http://keycloak:8080</proxy-url> <forced-route-url>http://keycloak:8080</forced-route-url>
<auth-server-url>http://localhost:${docker.tests.keycloakPort}/auth</auth-server-url> <auth-server-url>http://localhost:${docker.tests.keycloakPort}/auth</auth-server-url>
<realm>test</realm> <realm>test</realm>
<resource>alfresco-share</resource> <resource>alfresco-share</resource>

View File

@ -40,7 +40,7 @@
<alfresco-resource-name>alfresco</alfresco-resource-name> <alfresco-resource-name>alfresco</alfresco-resource-name>
</keycloak-auth-config> </keycloak-auth-config>
<keycloak-adapter-config> <keycloak-adapter-config>
<proxy-url>http://keycloak:8080</proxy-url> <forced-route-url>http://keycloak:8080</forced-route-url>
<!-- by default use the same client as alfresco (not really "clean") --> <!-- by default use the same client as alfresco (not really "clean") -->
<auth-server-url>http://localhost:8180/auth</auth-server-url> <auth-server-url>http://localhost:8180/auth</auth-server-url>
<realm>alfresco</realm> <realm>alfresco</realm>