Handle alfRedirectUrl parameter on login page

- some features (like QuickShare) may use it to trigger login with a pre-defined post authentication location
This commit is contained in:
AFaust
2025-02-23 21:30:51 +01:00
committed by Axel Faust
parent 4b1b0cbd08
commit ab95cdc2f9
3 changed files with 155 additions and 29 deletions

View File

@@ -33,6 +33,15 @@
<class>de.acosix.alfresco.keycloak.share.remote.AccessTokenAwareAlfrescoAuthenticator</class> <class>de.acosix.alfresco.keycloak.share.remote.AccessTokenAwareAlfrescoAuthenticator</class>
</authenticator> </authenticator>
<endpoint>
<id>alfresco-noauth</id>
<name>Alfresco - unauthenticated access</name>
<description>Access to Alfresco Repository WebScripts that do not require authentication</description>
<connector-id>alfresco</connector-id>
<endpoint-url>http://repository:8080/alfresco/s</endpoint-url>
<identity>none</identity>
</endpoint>
<endpoint> <endpoint>
<id>alfresco</id> <id>alfresco</id>
<name>Alfresco - user access</name> <name>Alfresco - user access</name>
@@ -71,7 +80,7 @@
<keycloak-auth-config> <keycloak-auth-config>
<enhance-login-form>true</enhance-login-form> <enhance-login-form>true</enhance-login-form>
<enable-sso-filter>true</enable-sso-filter> <enable-sso-filter>true</enable-sso-filter>
<force-keycloak-sso>false</force-keycloak-sso> <force-keycloak-sso>true</force-keycloak-sso>
<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>
@@ -88,4 +97,61 @@
</keycloak-adapter-config> </keycloak-adapter-config>
</config> </config>
<!-- Must be specified (typically provided as part of packaging) -->
<config evaluator="string-compare" condition="DocumentLibrary" replace="true">
<tree>
<evaluate-child-folders>false</evaluate-child-folders>
<maximum-folder-count>1000</maximum-folder-count>
<timeout>7000</timeout>
</tree>
<aspects>
<!-- Aspects that a user can see -->
<visible>
<aspect name="cm:generalclassifiable" />
<aspect name="cm:complianceable" />
<aspect name="cm:dublincore" />
<aspect name="cm:effectivity" />
<aspect name="cm:summarizable" />
<aspect name="cm:versionable" />
<aspect name="cm:templatable" />
<aspect name="cm:emailed" />
<aspect name="emailserver:aliasable" />
<aspect name="cm:taggable" />
<aspect name="app:inlineeditable" />
<aspect name="cm:geographic" />
<aspect name="exif:exif" />
<aspect name="audio:audio" />
<aspect name="cm:indexControl" />
<aspect name="dp:restrictable" />
<aspect name="smf:customConfigSmartFolder" />
<aspect name="smf:systemConfigSmartFolder" />
</visible>
<addable>
</addable>
<removeable>
</removeable>
</aspects>
<types>
<type name="cm:content">
<subtype name="smf:smartFolderTemplate" />
</type>
<type name="cm:folder">
</type>
<type name="trx:transferTarget">
<subtype name="trx:fileTransferTarget" />
</type>
</types>
<repository-url>http://repository:8080/alfresco</repository-url>
<google-docs>
<enabled>false</enabled>
<creatable-types>
<creatable type="doc">application/vnd.openxmlformats-officedocument.wordprocessingml.document</creatable>
<creatable type="xls">application/vnd.openxmlformats-officedocument.spreadsheetml.sheet</creatable>
<creatable type="ppt">application/vnd.ms-powerpoint</creatable>
</creatable-types>
</google-docs>
<file-upload>
<adobe-flash-enabled>false</adobe-flash-enabled>
</file-upload>
</config>
</alfresco-config> </alfresco-config>

View File

@@ -179,6 +179,9 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
private static final String PAGE_TYPE_PARAMETER_NAME = "pt"; private static final String PAGE_TYPE_PARAMETER_NAME = "pt";
// used on some login page redirects - see PageView ALF_REDIRECT_URL constant
private static final String ALF_REDIRECT_URL = "alfRedirectUrl";
private static final String LOGIN_PATH_INFORMATION = "/dologin"; private static final String LOGIN_PATH_INFORMATION = "/dologin";
private static final String LOGOUT_PATH_INFORMATION = "/dologout"; private static final String LOGOUT_PATH_INFORMATION = "/dologout";
@@ -527,13 +530,20 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
this.keycloakDeployment = KeycloakDeploymentBuilder.build(adapterConfiguration); this.keycloakDeployment = KeycloakDeploymentBuilder.build(adapterConfiguration);
// we need to recreate the HttpClient to configure the forced route URL // we need to recreate the HttpClient to configure the forced route URL
this.keycloakDeployment.setClient(new Callable<HttpClient>() { this.keycloakDeployment.setClient(new Callable<HttpClient>()
{
private HttpClient client; private HttpClient client;
@Override @Override
public HttpClient call() throws Exception { public HttpClient call() throws Exception
if (this.client == null) { {
synchronized (this) { if (this.client == null)
if (this.client == null) { {
synchronized (this)
{
if (this.client == null)
{
this.client = new HttpClientBuilder() this.client = new HttpClientBuilder()
.routePlanner(KeycloakAuthenticationFilter.this.createForcedRoutePlanner(adapterConfiguration)) .routePlanner(KeycloakAuthenticationFilter.this.createForcedRoutePlanner(adapterConfiguration))
.build(adapterConfiguration); .build(adapterConfiguration);
@@ -845,11 +855,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
return cookie; return cookie;
}).forEach(res::addCookie); }).forEach(res::addCookie);
final List<String> redirects = captureFacade.getHeaders().get("Location"); setRedirectFromCaptureFacade(req, captureFacade);
if (redirects != null && !redirects.isEmpty())
{
LOGIN_REDIRECT_URL.set(redirects.get(0));
}
} }
/** /**
@@ -881,6 +887,14 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
{ {
// no query parameters, so no code= and no error= // no query parameters, so no code= and no error=
// this will cause login redirect challenge to be generated // this will cause login redirect challenge to be generated
// but use the alfRedirectUrl if present in request
final String alfRedirectUrl = req.getParameter(ALF_REDIRECT_URL);
if (alfRedirectUrl != null && !alfRedirectUrl.isBlank())
{
LOGGER.debug("Found {} query parameter with value {}", ALF_REDIRECT_URL, alfRedirectUrl);
return ALF_REDIRECT_URL + "=" + alfRedirectUrl;
}
return ""; return "";
} }
@@ -915,10 +929,17 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
return cookie; return cookie;
}).forEach(res::addCookie); }).forEach(res::addCookie);
setRedirectFromCaptureFacade(req, captureFacade);
}
private static void setRedirectFromCaptureFacade(final HttpServletRequest req,
final ResponseHeaderCookieCaptureServletHttpFacade captureFacade)
{
final List<String> redirects = captureFacade.getHeaders().get("Location"); final List<String> redirects = captureFacade.getHeaders().get("Location");
if (redirects != null && !redirects.isEmpty()) if (redirects != null && !redirects.isEmpty())
{ {
LOGIN_REDIRECT_URL.set(redirects.get(0)); final String redirectPath = redirects.get(0);
LOGIN_REDIRECT_URL.set(redirectPath);
} }
} }
@@ -961,9 +982,19 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
this.handleAlfrescoResourceAccessToken(session); this.handleAlfrescoResourceAccessToken(session);
} }
final String alfRedirectUrl = req.getParameter(ALF_REDIRECT_URL);
if (facade.isEnded()) if (facade.isEnded())
{ {
LOGGER.debug("Authenticator already handled response"); LOGGER.debug("Authenticator already handled response");
if (alfRedirectUrl != null && !alfRedirectUrl.isBlank())
{
LOGGER.debug("Found {} query parameter - redirecting to {}", ALF_REDIRECT_URL, alfRedirectUrl);
// this may override any redirect set by the authenticator
res.sendRedirect(alfRedirectUrl);
}
return; return;
} }
@@ -1022,9 +1053,19 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
session.setAttribute(UserFactory.SESSION_ATTRIBUTE_EXTERNAL_AUTH, Boolean.TRUE); session.setAttribute(UserFactory.SESSION_ATTRIBUTE_EXTERNAL_AUTH, Boolean.TRUE);
session.setAttribute(UserFactory.SESSION_ATTRIBUTE_KEY_USER_ID, userId); session.setAttribute(UserFactory.SESSION_ATTRIBUTE_KEY_USER_ID, userId);
final String alfRedirectUrl = req.getParameter(ALF_REDIRECT_URL);
if (facade.isEnded()) if (facade.isEnded())
{ {
LOGGER.debug("Authenticator already handled response"); LOGGER.debug("Authenticator already handled response");
if (alfRedirectUrl != null && !alfRedirectUrl.isBlank())
{
LOGGER.debug("Found {} query parameter - redirecting to {}", ALF_REDIRECT_URL, alfRedirectUrl);
// this may override any redirect set by the authenticator
res.sendRedirect(alfRedirectUrl);
}
return; return;
} }
@@ -1042,8 +1083,6 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
} }
} }
this.completeRequestContext(req);
LOGGER.debug("Continueing with filter chain processing"); LOGGER.debug("Continueing with filter chain processing");
this.continueFilterChain(context, req, res, chain); this.continueFilterChain(context, req, res, chain);
} }
@@ -1773,11 +1812,9 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
final List<Header> headers = new LinkedList<>(); final List<Header> headers = new LinkedList<>();
ClientCredentialsProviderUtils.setClientCredentials( ClientCredentialsProviderUtils.setClientCredentials(this.keycloakDeployment.getAdapterConfig(),
this.keycloakDeployment.getAdapterConfig(), this.keycloakDeployment.getClientAuthenticator(), new NameValueMapAdapter<>(headers, BasicHeader.class),
this.keycloakDeployment.getClientAuthenticator(), new NameValueMapAdapter<>(formParams, BasicNameValuePair.class));
new NameValueMapAdapter<>(headers, BasicHeader.class),
new NameValueMapAdapter<>(formParams, BasicNameValuePair.class));
for (final Header header : headers) for (final Header header : headers)
{ {
@@ -1900,41 +1937,54 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
params.setParameter(ConnRoutePNames.FORCED_ROUTE, route); params.setParameter(ConnRoutePNames.FORCED_ROUTE, route);
} }
protected HttpRoute createRoute(final ExtendedAdapterConfig adapterConfig, final HttpHost routeHost) throws UnknownHostException, MalformedURLException { protected HttpRoute createRoute(final ExtendedAdapterConfig adapterConfig, final HttpHost routeHost)
throws UnknownHostException, MalformedURLException
{
final boolean secure = "https".equalsIgnoreCase(routeHost.getSchemeName()); final boolean secure = "https".equalsIgnoreCase(routeHost.getSchemeName());
if (adapterConfig.getProxyUrl() != null) { if (adapterConfig.getProxyUrl() != null)
{
// useful in parsing the URL for just what is needed for HttpHost // useful in parsing the URL for just what is needed for HttpHost
final URL proxyUrl = new URL(adapterConfig.getProxyUrl()); final URL proxyUrl = new URL(adapterConfig.getProxyUrl());
final HttpHost proxyHost = new HttpHost(proxyUrl.getHost(), proxyUrl.getPort(), proxyUrl.getProtocol()); final HttpHost proxyHost = new HttpHost(proxyUrl.getHost(), proxyUrl.getPort(), proxyUrl.getProtocol());
return new HttpRoute(routeHost, InetAddress.getLocalHost(), proxyHost, secure); return new HttpRoute(routeHost, InetAddress.getLocalHost(), proxyHost, secure);
} else { }
else
{
return new HttpRoute(routeHost, InetAddress.getLocalHost(), secure); return new HttpRoute(routeHost, InetAddress.getLocalHost(), secure);
} }
} }
protected HttpRoute createForcedRoute(final ExtendedAdapterConfig adapterConfig) throws UnknownHostException, MalformedURLException { protected HttpRoute createForcedRoute(final ExtendedAdapterConfig adapterConfig) throws UnknownHostException, MalformedURLException
{
// useful in parsing the URL for just what is needed for HttpHost // useful in parsing the URL for just what is needed for HttpHost
final URL forcedRouteUrl = new URL(adapterConfig.getForcedRouteUrl()); final URL forcedRouteUrl = new URL(adapterConfig.getForcedRouteUrl());
final HttpHost forcedRouteHost = new HttpHost(forcedRouteUrl.getHost(), forcedRouteUrl.getPort(), forcedRouteUrl.getProtocol()); final HttpHost forcedRouteHost = new HttpHost(forcedRouteUrl.getHost(), forcedRouteUrl.getPort(), forcedRouteUrl.getProtocol());
return this.createRoute(adapterConfig, forcedRouteHost); return this.createRoute(adapterConfig, forcedRouteHost);
} }
protected HttpRoutePlanner createForcedRoutePlanner(final ExtendedAdapterConfig adapterConfig) throws MalformedURLException { protected HttpRoutePlanner createForcedRoutePlanner(final ExtendedAdapterConfig adapterConfig) throws MalformedURLException
{
final URL authServerUrl = new URL(adapterConfig.getAuthServerUrl()); final URL authServerUrl = new URL(adapterConfig.getAuthServerUrl());
final HttpHost authServerHost = new HttpHost(authServerUrl.getHost(), authServerUrl.getPort(), authServerUrl.getProtocol()); final HttpHost authServerHost = new HttpHost(authServerUrl.getHost(), authServerUrl.getPort(), authServerUrl.getProtocol());
return (target, request, context) -> { return (target, request, context) -> {
try { try
if (authServerHost.equals(target)) { {
if (authServerHost.equals(target))
{
LOGGER.trace("Rerouting to forced route"); LOGGER.trace("Rerouting to forced route");
final HttpRoute route = KeycloakAuthenticationFilter.this.createForcedRoute(adapterConfig); final HttpRoute route = KeycloakAuthenticationFilter.this.createForcedRoute(adapterConfig);
LOGGER.trace("Rerouting to forced route: {}", route); LOGGER.trace("Rerouting to forced route: {}", route);
return route; return route;
} else { }
else
{
return KeycloakAuthenticationFilter.this.createRoute(adapterConfig, target); return KeycloakAuthenticationFilter.this.createRoute(adapterConfig, target);
} }
} catch (final IOException ie) { }
catch (final IOException ie)
{
throw new HttpException(ie.getMessage(), ie); throw new HttpException(ie.getMessage(), ie);
} }
}; };

View File

@@ -44,7 +44,17 @@ function main()
parameterModel.value = decodeURIComponent(parameter.substring(parameter.indexOf('=') + 1)); parameterModel.value = decodeURIComponent(parameter.substring(parameter.indexOf('=') + 1));
if (parameterModel.value.indexOf('?') !== -1) if (parameterModel.value.indexOf('?') !== -1)
{ {
parameterModel.value = parameterModel.value.substring(0, parameterModel.value.indexOf('?')); if (parameterModel.value.indexOf('?alfRedirectUrl=') !== -1)
{
if (parameterModel.value.indexOf('&') !== -1)
{
parameterModel.value = parameterModel.value.substring(0, parameterModel.value.indexOf('&'));
}
}
else
{
parameterModel.value = parameterModel.value.substring(0, parameterModel.value.indexOf('?'));
}
} }
} }
else else