mirror of
https://github.com/bmlong137/alfresco-keycloak.git
synced 2025-05-12 21:24:43 +00:00
Add token exchange support for Share/Repo integration
This commit is contained in:
parent
32c4fabff0
commit
9d9f665f29
15
pom.xml
15
pom.xml
@ -219,6 +219,21 @@
|
|||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>de.acosix.alfresco.utility</groupId>
|
||||||
|
<artifactId>de.acosix.alfresco.utility.share</artifactId>
|
||||||
|
<version>${acosix.utility.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>de.acosix.alfresco.utility</groupId>
|
||||||
|
<artifactId>de.acosix.alfresco.utility.share</artifactId>
|
||||||
|
<version>${acosix.utility.version}</version>
|
||||||
|
<classifier>installable</classifier>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.orderofthebee.support-tools</groupId>
|
<groupId>org.orderofthebee.support-tools</groupId>
|
||||||
<artifactId>support-tools-repo</artifactId>
|
<artifactId>support-tools-repo</artifactId>
|
||||||
|
@ -121,7 +121,7 @@
|
|||||||
<!-- no change to Search image -->
|
<!-- no change to Search image -->
|
||||||
</image>
|
</image>
|
||||||
<image>
|
<image>
|
||||||
<name>jboss/keycloak</name>
|
<name>jboss/keycloak:${keycloak.version}</name>
|
||||||
<alias>keycloak</alias>
|
<alias>keycloak</alias>
|
||||||
<run>
|
<run>
|
||||||
<hostname>keycloak</hostname>
|
<hostname>keycloak</hostname>
|
||||||
|
@ -285,12 +285,12 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
|
|||||||
throw new AuthenticationException("Failed to refresh Keycloak authentication", ioex);
|
throw new AuthenticationException("Failed to refresh Keycloak authentication", ioex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (this.failExpiredTicketTokens && ticketToken.isExpired())
|
else if (this.failExpiredTicketTokens && !ticketToken.isActive())
|
||||||
{
|
{
|
||||||
throw new AuthenticationException("Keycloak access token has expired - authentication ticket is no longer valid");
|
throw new AuthenticationException("Keycloak access token has expired - authentication ticket is no longer valid");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result != null || !ticketToken.isExpired())
|
if (result != null || ticketToken.isActive())
|
||||||
{
|
{
|
||||||
this.handleUserTokens(result != null ? result.getAccessToken() : ticketToken.getAccessToken(),
|
this.handleUserTokens(result != null ? result.getAccessToken() : ticketToken.getAccessToken(),
|
||||||
result != null ? result.getIdToken() : ticketToken.getIdToken(), false);
|
result != null ? result.getIdToken() : ticketToken.getIdToken(), false);
|
||||||
|
@ -67,6 +67,7 @@ import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.spi.Authenticatio
|
|||||||
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.spi.KeycloakAccount;
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.spi.KeycloakAccount;
|
||||||
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.spi.SessionIdMapper;
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.spi.SessionIdMapper;
|
||||||
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.spi.UserSessionManagement;
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.spi.UserSessionManagement;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.common.util.KeycloakUriBuilder;
|
||||||
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.AccessToken;
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.AccessToken;
|
||||||
import de.acosix.alfresco.keycloak.repo.util.AlfrescoCompatibilityUtil;
|
import de.acosix.alfresco.keycloak.repo.util.AlfrescoCompatibilityUtil;
|
||||||
import de.acosix.alfresco.keycloak.repo.util.RefreshableAccessTokenHolder;
|
import de.acosix.alfresco.keycloak.repo.util.RefreshableAccessTokenHolder;
|
||||||
@ -248,7 +249,15 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
|
|||||||
final HttpServletRequest req = (HttpServletRequest) request;
|
final HttpServletRequest req = (HttpServletRequest) request;
|
||||||
final HttpServletResponse res = (HttpServletResponse) response;
|
final HttpServletResponse res = (HttpServletResponse) response;
|
||||||
|
|
||||||
final boolean skip = this.checkForSkipCondition(context, req, res);
|
final KeycloakUriBuilder authUrl = this.keycloakDeployment.getAuthUrl();
|
||||||
|
final boolean keycloakDeploymentReady = authUrl != null;
|
||||||
|
if (!keycloakDeploymentReady)
|
||||||
|
{
|
||||||
|
LOGGER.warn("Cannot process Keycloak-specifics as Keycloak library was unable to resolve relative URLs from {}",
|
||||||
|
this.keycloakDeployment.getAuthServerBaseUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean skip = !keycloakDeploymentReady || this.checkForSkipCondition(context, req, res);
|
||||||
|
|
||||||
if (skip)
|
if (skip)
|
||||||
{
|
{
|
||||||
@ -635,53 +644,96 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
|
|||||||
|
|
||||||
if (!this.active)
|
if (!this.active)
|
||||||
{
|
{
|
||||||
LOGGER.trace("Skipping doFilter as filter is not active");
|
LOGGER.trace("Skipping processKeycloakAuthenticationAndActions as filter is not active");
|
||||||
skip = true;
|
skip = true;
|
||||||
}
|
}
|
||||||
else if (servletRequestUri.matches(KEYCLOAK_ACTION_URL_PATTERN))
|
else if (servletRequestUri.matches(KEYCLOAK_ACTION_URL_PATTERN))
|
||||||
{
|
{
|
||||||
LOGGER.trace("Explicitly not skipping doFilter as Keycloak action URL is being called");
|
LOGGER.trace("Explicitly not skipping processKeycloakAuthenticationAndActions as Keycloak action URL is being called");
|
||||||
}
|
}
|
||||||
else if (req.getParameter("state") != null && req.getParameter("code") != null && this.hasStateCookie(req))
|
else if (req.getParameter("state") != null && req.getParameter("code") != null && this.hasStateCookie(req))
|
||||||
{
|
{
|
||||||
LOGGER.trace(
|
LOGGER.trace(
|
||||||
"Explicitly not skipping doFilter as state and code query parameters of OAuth2 redirect as well as state cookie are present");
|
"Explicitly not skipping processKeycloakAuthenticationAndActions as state and code query parameters of OAuth2 redirect as well as state cookie are present");
|
||||||
}
|
}
|
||||||
else if (authHeader != null && authHeader.toLowerCase(Locale.ENGLISH).startsWith("bearer "))
|
else if (authHeader != null && authHeader.toLowerCase(Locale.ENGLISH).startsWith("bearer "))
|
||||||
{
|
{
|
||||||
LOGGER.trace("Explicitly not skipping doFilter as Bearer authorization header is present");
|
final AccessToken accessToken = (AccessToken) session.getAttribute(KeycloakRemoteUserMapper.class.getName());
|
||||||
|
if (accessToken != null)
|
||||||
|
{
|
||||||
|
if (accessToken.isActive())
|
||||||
|
{
|
||||||
|
LOGGER.trace(
|
||||||
|
"Skipping processKeycloakAuthenticationAndActions as Bearer authorization header for {} has already been processed by remote user mapper",
|
||||||
|
AlfrescoCompatibilityUtil.maskUsername(accessToken.getPreferredUsername()));
|
||||||
|
this.keycloakAuthenticationComponent.handleUserTokens(accessToken, accessToken, session.isNew());
|
||||||
|
skip = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGER.trace(
|
||||||
|
"Explicitly not skipping processKeycloakAuthenticationAndActions as processed Bearer authorization token for {} has expired",
|
||||||
|
AlfrescoCompatibilityUtil.maskUsername(accessToken.getPreferredUsername()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGER.trace(
|
||||||
|
"Explicitly not skipping processKeycloakAuthenticationAndActions as unprocessed Bearer authorization header is present");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (authHeader != null && authHeader.toLowerCase(Locale.ENGLISH).startsWith("basic "))
|
else if (authHeader != null && authHeader.toLowerCase(Locale.ENGLISH).startsWith("basic "))
|
||||||
{
|
{
|
||||||
LOGGER.trace("Explicitly not skipping doFilter as Basic authorization header is present");
|
LOGGER.trace("Explicitly not skipping processKeycloakAuthenticationAndActions as Basic authorization header is present");
|
||||||
}
|
}
|
||||||
else if (authHeader != null)
|
else if (authHeader != null)
|
||||||
{
|
{
|
||||||
LOGGER.trace("Skipping doFilter as non-OIDC / non-Basic authorization header is present");
|
LOGGER.trace("Skipping processKeycloakAuthenticationAndActions as non-OIDC / non-Basic authorization header is present");
|
||||||
skip = true;
|
skip = true;
|
||||||
}
|
}
|
||||||
else if (this.allowTicketLogon && this.checkForTicketParameter(context, req, res))
|
else if (this.allowTicketLogon && this.checkForTicketParameter(context, req, res))
|
||||||
{
|
{
|
||||||
LOGGER.trace("Skipping doFilter as user was authenticated by ticket URL parameter");
|
LOGGER.trace("Skipping processKeycloakAuthenticationAndActions as user was authenticated by ticket URL parameter");
|
||||||
skip = true;
|
skip = true;
|
||||||
}
|
}
|
||||||
// check no-auth flag (derived e.g. from checking if target web script requires authentication) only after all pre-emptive auth
|
// check no-auth flag (derived e.g. from checking if target web script requires authentication) only after all pre-emptive auth
|
||||||
// request details have been checked
|
// request details have been checked
|
||||||
else if (Boolean.TRUE.equals(req.getAttribute(NO_AUTH_REQUIRED)))
|
else if (Boolean.TRUE.equals(req.getAttribute(NO_AUTH_REQUIRED)))
|
||||||
{
|
{
|
||||||
LOGGER.trace("Skipping doFilter as filter higher up in chain determined authentication as not required");
|
LOGGER.trace(
|
||||||
|
"Skipping processKeycloakAuthenticationAndActions as filter higher up in chain determined authentication as not required");
|
||||||
skip = true;
|
skip = true;
|
||||||
}
|
}
|
||||||
else if (sessionUser != null)
|
else if (sessionUser != null)
|
||||||
{
|
{
|
||||||
final KeycloakAccount keycloakAccount = (KeycloakAccount) session.getAttribute(KeycloakAccount.class.getName());
|
final KeycloakAccount keycloakAccount = (KeycloakAccount) session.getAttribute(KeycloakAccount.class.getName());
|
||||||
|
final AccessToken accessToken = (AccessToken) session.getAttribute(KeycloakRemoteUserMapper.class.getName());
|
||||||
if (keycloakAccount != null)
|
if (keycloakAccount != null)
|
||||||
{
|
{
|
||||||
skip = this.validateAndRefreshKeycloakAuthentication(req, res, sessionUser.getUserName());
|
skip = this.validateAndRefreshKeycloakAuthentication(req, res, sessionUser.getUserName());
|
||||||
}
|
}
|
||||||
|
else if (accessToken != null)
|
||||||
|
{
|
||||||
|
if (accessToken.isActive())
|
||||||
|
{
|
||||||
|
LOGGER.trace(
|
||||||
|
"Skipping processKeycloakAuthenticationAndActions as access token in session from previous Bearer authorization for {} is still valid",
|
||||||
|
AlfrescoCompatibilityUtil.maskUsername(sessionUser.getUserName()));
|
||||||
|
this.keycloakAuthenticationComponent.handleUserTokens(accessToken, accessToken, false);
|
||||||
|
skip = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGER.trace(
|
||||||
|
"Explicitly not skipping processKeycloakAuthenticationAndActions as access token in session from previous Bearer authorization for {} has expired",
|
||||||
|
AlfrescoCompatibilityUtil.maskUsername(sessionUser.getUserName()));
|
||||||
|
this.invalidateSession(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LOGGER.trace("Skipping doFilter as non-Keycloak-authenticated session is already established");
|
LOGGER.trace(
|
||||||
|
"Skipping processKeycloakAuthenticationAndActions as non-Keycloak-authenticated session is already established");
|
||||||
skip = true;
|
skip = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,7 +134,7 @@ public class KeycloakAuthenticationServiceImpl extends AuthenticationServiceImpl
|
|||||||
this.keycloakTicketTokenCache.put(ticket, refreshedToken);
|
this.keycloakTicketTokenCache.put(ticket, refreshedToken);
|
||||||
}
|
}
|
||||||
// apparently expiration is allowed - remove from cache to avoid unnecessary checks in the future
|
// apparently expiration is allowed - remove from cache to avoid unnecessary checks in the future
|
||||||
else if (refreshableAccessToken.isExpired())
|
else if (!refreshableAccessToken.isActive())
|
||||||
{
|
{
|
||||||
LOGGER.warn(
|
LOGGER.warn(
|
||||||
"The Keycloak access token associated with ticket {} for user {} has expired - Keycloak roles / claims are no longer available for the corresponding user",
|
"The Keycloak access token associated with ticket {} for user {} has expired - Keycloak roles / claims are no longer available for the corresponding user",
|
||||||
|
@ -18,6 +18,7 @@ package de.acosix.alfresco.keycloak.repo.authentication;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationException;
|
import org.alfresco.repo.security.authentication.AuthenticationException;
|
||||||
@ -32,6 +33,7 @@ import org.springframework.beans.factory.InitializingBean;
|
|||||||
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.BearerTokenRequestAuthenticator;
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.BearerTokenRequestAuthenticator;
|
||||||
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.KeycloakDeployment;
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.KeycloakDeployment;
|
||||||
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.spi.AuthOutcome;
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.spi.AuthOutcome;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.AccessToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Axel Faust
|
* @author Axel Faust
|
||||||
@ -120,11 +122,22 @@ public class KeycloakRemoteUserMapper implements RemoteUserMapper, ActivateableB
|
|||||||
final BearerTokenRequestAuthenticator authenticator = new BearerTokenRequestAuthenticator(this.keycloakDeployment);
|
final BearerTokenRequestAuthenticator authenticator = new BearerTokenRequestAuthenticator(this.keycloakDeployment);
|
||||||
final AuthOutcome authOutcome = authenticator.authenticate(httpFacade);
|
final AuthOutcome authOutcome = authenticator.authenticate(httpFacade);
|
||||||
|
|
||||||
|
// TODO Check on how to enable / add client/audience validation
|
||||||
|
// currently, Share token seems to be valid here, which it shouldn't be
|
||||||
|
// also, Share token may not contain Alfresco client roles (e.g. admin)
|
||||||
if (authOutcome == AuthOutcome.AUTHENTICATED)
|
if (authOutcome == AuthOutcome.AUTHENTICATED)
|
||||||
{
|
{
|
||||||
final String preferredUsername = authenticator.getToken().getPreferredUsername();
|
final AccessToken token = authenticator.getToken();
|
||||||
final String normalisedUserName = AuthenticationUtil
|
final String preferredUsername = token.getPreferredUsername();
|
||||||
.runAsSystem(() -> this.personService.getUserIdentifier(preferredUsername));
|
|
||||||
|
// need to store token for later validation
|
||||||
|
final HttpSession session = request.getSession(true);
|
||||||
|
session.setAttribute(KeycloakRemoteUserMapper.class.getName(), token);
|
||||||
|
|
||||||
|
// need case distinction to avoid user name being nulled when user does not exist yet
|
||||||
|
final String normalisedUserName = AuthenticationUtil.runAsSystem(
|
||||||
|
() -> this.personService.personExists(preferredUsername) ? this.personService.getUserIdentifier(preferredUsername)
|
||||||
|
: preferredUsername);
|
||||||
|
|
||||||
LOGGER.debug("Authenticated user {} via bearer token, normalised as {}", preferredUsername, normalisedUserName);
|
LOGGER.debug("Authenticated user {} via bearer token, normalised as {}", preferredUsername, normalisedUserName);
|
||||||
|
|
||||||
|
@ -236,6 +236,10 @@ public class KeycloakAdapterConfigBeanFactory implements FactoryBean<AdapterConf
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
PropertyCheck.mandatory(adapterConfig, "auth-server-url", adapterConfig.getAuthServerUrl());
|
||||||
|
PropertyCheck.mandatory(adapterConfig, "realm", adapterConfig.getRealm());
|
||||||
|
PropertyCheck.mandatory(adapterConfig, "resource", adapterConfig.getResource());
|
||||||
|
|
||||||
return adapterConfig;
|
return adapterConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +96,17 @@ public class RefreshableAccessTokenHolder implements Serializable
|
|||||||
this.refreshExpiration = Time.currentTime() - (accessToken.getExpiration() - Time.currentTime()) / 100;
|
this.refreshExpiration = Time.currentTime() - (accessToken.getExpiration() - Time.currentTime()) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the encapsulated access token is active.
|
||||||
|
*
|
||||||
|
* @return {@code true} if the access token is active, {@code false} otherwise
|
||||||
|
*/
|
||||||
|
public boolean isActive()
|
||||||
|
{
|
||||||
|
final boolean isActive = this.accessToken.isActive();
|
||||||
|
return isActive;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the encapsulated access token has expired.
|
* Checks whether the encapsulated access token has expired.
|
||||||
*
|
*
|
||||||
@ -103,7 +114,7 @@ public class RefreshableAccessTokenHolder implements Serializable
|
|||||||
*/
|
*/
|
||||||
public boolean isExpired()
|
public boolean isExpired()
|
||||||
{
|
{
|
||||||
final boolean isExpired = this.accessToken.getExpiration() < Time.currentTime();
|
final boolean isExpired = this.accessToken.isExpired();
|
||||||
return isExpired;
|
return isExpired;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.authentication.directAuthHost=http://host.docker.internal:8380
|
keycloak.authentication.directAuthHost=http://keycloak:8080
|
||||||
|
|
||||||
keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=/Test A
|
keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=/Test A
|
||||||
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=/Test A
|
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=/Test A
|
||||||
|
@ -97,24 +97,12 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>de.acosix.alfresco.utility</groupId>
|
<groupId>de.acosix.alfresco.utility</groupId>
|
||||||
<artifactId>de.acosix.alfresco.utility.core.share</artifactId>
|
<artifactId>de.acosix.alfresco.utility.core.share</artifactId>
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.slf4j</groupId>
|
|
||||||
<artifactId>slf4j-log4j12</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>de.acosix.alfresco.utility</groupId>
|
<groupId>de.acosix.alfresco.utility</groupId>
|
||||||
<artifactId>de.acosix.alfresco.utility.core.share</artifactId>
|
<artifactId>de.acosix.alfresco.utility.core.share</artifactId>
|
||||||
<classifier>installable</classifier>
|
<classifier>installable</classifier>
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>org.slf4j</groupId>
|
|
||||||
<artifactId>slf4j-log4j12</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -199,7 +187,7 @@
|
|||||||
<!-- no change to Search image -->
|
<!-- no change to Search image -->
|
||||||
</image>
|
</image>
|
||||||
<image>
|
<image>
|
||||||
<name>jboss/keycloak</name>
|
<name>jboss/keycloak:${keycloak.version}</name>
|
||||||
<alias>keycloak</alias>
|
<alias>keycloak</alias>
|
||||||
<run>
|
<run>
|
||||||
<hostname>keycloak</hostname>
|
<hostname>keycloak</hostname>
|
||||||
@ -219,6 +207,7 @@
|
|||||||
</network>
|
</network>
|
||||||
<volumes>
|
<volumes>
|
||||||
<bind>
|
<bind>
|
||||||
|
<volume>${project.build.directory}/docker/keycloakProfile.properties:/opt/jboss/keycloak/standalone/configuration/profile.properties</volume>
|
||||||
<volume>${project.build.directory}/docker/test-realm.json:/tmp/test-realm.json</volume>
|
<volume>${project.build.directory}/docker/test-realm.json:/tmp/test-realm.json</volume>
|
||||||
</bind>
|
</bind>
|
||||||
</volumes>
|
</volumes>
|
||||||
|
@ -35,8 +35,12 @@
|
|||||||
<ssl-redirect-port>8443</ssl-redirect-port>
|
<ssl-redirect-port>8443</ssl-redirect-port>
|
||||||
<body-buffer-limit>10485760</body-buffer-limit>
|
<body-buffer-limit>10485760</body-buffer-limit>
|
||||||
<session-mapper-limit>1000</session-mapper-limit>
|
<session-mapper-limit>1000</session-mapper-limit>
|
||||||
|
<ignore-default-filter>true</ignore-default-filter>
|
||||||
|
<perform-token-exchange>false</perform-token-exchange>
|
||||||
|
<alfresco-resource-name>alfresco</alfresco-resource-name>
|
||||||
</keycloak-auth-config>
|
</keycloak-auth-config>
|
||||||
<keycloak-adapter-config>
|
<keycloak-adapter-config>
|
||||||
|
<!-- 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>
|
||||||
<resource>alfresco</resource>
|
<resource>alfresco</resource>
|
||||||
|
@ -46,7 +46,15 @@
|
|||||||
</list>
|
</list>
|
||||||
</property>
|
</property>
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
|
<bean id="${moduleId}.maxRemoteClientRedirectPatch"
|
||||||
|
class="de.acosix.alfresco.utility.common.spring.PropertyAlteringBeanFactoryPostProcessor">
|
||||||
|
<property name="targetBeanName" value="connector.remoteclient.abstract" />
|
||||||
|
<property name="propertyName" value="maxRedirects" />
|
||||||
|
<property name="value" value="1" />
|
||||||
|
<property name="enabled" value="\${${moduleId}.surf.onlyOneRedirect.enabled}" />
|
||||||
|
</bean>
|
||||||
|
|
||||||
<bean id="${moduleId}.SessionIdMapper" class="${project.artifactId}.web.DefaultSessionIdMapper">
|
<bean id="${moduleId}.SessionIdMapper" class="${project.artifactId}.web.DefaultSessionIdMapper">
|
||||||
<property name="configService" ref="web.config" />
|
<property name="configService" ref="web.config" />
|
||||||
</bean>
|
</bean>
|
||||||
@ -69,7 +77,7 @@
|
|||||||
<property name="configService" ref="web.config" />
|
<property name="configService" ref="web.config" />
|
||||||
<property name="connectorService" ref="connector.service" />
|
<property name="connectorService" ref="connector.service" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean class="${project.artifactId}.spring.KeycloakAuthenticationFilterActivation">
|
<bean class="${project.artifactId}.spring.KeycloakAuthenticationFilterActivation">
|
||||||
<property name="moduleId" value="${moduleId}" />
|
<property name="moduleId" value="${moduleId}" />
|
||||||
</bean>
|
</bean>
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
${moduleId}.surf.onlyOneRedirect.enabled=true
|
@ -31,6 +31,7 @@ import java.util.Set;
|
|||||||
import org.alfresco.error.AlfrescoRuntimeException;
|
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.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;
|
||||||
@ -318,7 +319,7 @@ public class KeycloakAdapterConfigElement extends BaseCustomConfigElement
|
|||||||
*/
|
*/
|
||||||
public AdapterConfig buildAdapterConfiguration()
|
public AdapterConfig buildAdapterConfiguration()
|
||||||
{
|
{
|
||||||
final AdapterConfig config = new AdapterConfig();
|
final AdapterConfig adapterConfig = new AdapterConfig();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -328,7 +329,7 @@ public class KeycloakAdapterConfigElement extends BaseCustomConfigElement
|
|||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
final Method setter = SETTER_BY_CONFIG_NAME.get(configName);
|
final Method setter = SETTER_BY_CONFIG_NAME.get(configName);
|
||||||
setter.invoke(config, value);
|
setter.invoke(adapterConfig, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -337,7 +338,11 @@ public class KeycloakAdapterConfigElement extends BaseCustomConfigElement
|
|||||||
throw new AlfrescoRuntimeException("Error building adapter configuration", ex);
|
throw new AlfrescoRuntimeException("Error building adapter configuration", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
PropertyCheck.mandatory(adapterConfig, "auth-server-url", adapterConfig.getAuthServerUrl());
|
||||||
|
PropertyCheck.mandatory(adapterConfig, "realm", adapterConfig.getRealm());
|
||||||
|
PropertyCheck.mandatory(adapterConfig, "resource", adapterConfig.getResource());
|
||||||
|
|
||||||
|
return adapterConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,6 +43,12 @@ public class KeycloakAuthenticationConfigElement extends BaseCustomConfigElement
|
|||||||
|
|
||||||
protected final ConfigValueHolder<Integer> sessionMapperLimit = new ConfigValueHolder<>();
|
protected final ConfigValueHolder<Integer> sessionMapperLimit = new ConfigValueHolder<>();
|
||||||
|
|
||||||
|
protected final ConfigValueHolder<Boolean> ignoreDefaultFilter = new ConfigValueHolder<>();
|
||||||
|
|
||||||
|
protected final ConfigValueHolder<Boolean> performTokenExchange = new ConfigValueHolder<>();
|
||||||
|
|
||||||
|
protected final ConfigValueHolder<String> alfrescoResourceName = new ConfigValueHolder<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance of this class.
|
* Creates a new instance of this class.
|
||||||
*/
|
*/
|
||||||
@ -153,6 +159,57 @@ public class KeycloakAuthenticationConfigElement extends BaseCustomConfigElement
|
|||||||
return this.sessionMapperLimit.getValue();
|
return this.sessionMapperLimit.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ignoreDefaultFilter
|
||||||
|
* the ignoreDefaultFilter to set
|
||||||
|
*/
|
||||||
|
public void setIgnoreDefaultFilter(final Boolean ignoreDefaultFilter)
|
||||||
|
{
|
||||||
|
this.ignoreDefaultFilter.setValue(ignoreDefaultFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the ignoreDefaultFilter
|
||||||
|
*/
|
||||||
|
public Boolean getIgnoreDefaultFilter()
|
||||||
|
{
|
||||||
|
return this.ignoreDefaultFilter.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param performTokenExchange
|
||||||
|
* the performTokenExchange to set
|
||||||
|
*/
|
||||||
|
public void setPerformTokenExchange(final Boolean performTokenExchange)
|
||||||
|
{
|
||||||
|
this.performTokenExchange.setValue(performTokenExchange);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the performTokenExchange
|
||||||
|
*/
|
||||||
|
public Boolean getPerformTokenExchange()
|
||||||
|
{
|
||||||
|
return this.performTokenExchange.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param alfrescoResourceName
|
||||||
|
* the alfrescoResourceName to set
|
||||||
|
*/
|
||||||
|
public void setAlfrescoResourceName(final String alfrescoResourceName)
|
||||||
|
{
|
||||||
|
this.alfrescoResourceName.setValue(alfrescoResourceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the alfrescoResourceName
|
||||||
|
*/
|
||||||
|
public String getAlfrescoResourceName()
|
||||||
|
{
|
||||||
|
return this.alfrescoResourceName.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
@ -228,6 +285,39 @@ public class KeycloakAuthenticationConfigElement extends BaseCustomConfigElement
|
|||||||
: this.getSessionMapperLimit());
|
: this.getSessionMapperLimit());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (otherConfigElement.ignoreDefaultFilter.isUnset())
|
||||||
|
{
|
||||||
|
combined.ignoreDefaultFilter.unset();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
combined.setIgnoreDefaultFilter(
|
||||||
|
otherConfigElement.getIgnoreDefaultFilter() != null ? otherConfigElement.getIgnoreDefaultFilter()
|
||||||
|
: this.getIgnoreDefaultFilter());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otherConfigElement.performTokenExchange.isUnset())
|
||||||
|
{
|
||||||
|
combined.performTokenExchange.unset();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
combined.setPerformTokenExchange(
|
||||||
|
otherConfigElement.getPerformTokenExchange() != null ? otherConfigElement.getPerformTokenExchange()
|
||||||
|
: this.getPerformTokenExchange());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otherConfigElement.alfrescoResourceName.isUnset())
|
||||||
|
{
|
||||||
|
combined.alfrescoResourceName.unset();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
combined.setAlfrescoResourceName(
|
||||||
|
otherConfigElement.getAlfrescoResourceName() != null ? otherConfigElement.getAlfrescoResourceName()
|
||||||
|
: this.getAlfrescoResourceName());
|
||||||
|
}
|
||||||
|
|
||||||
return combined;
|
return combined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,6 +346,15 @@ public class KeycloakAuthenticationConfigElement extends BaseCustomConfigElement
|
|||||||
builder.append(", ");
|
builder.append(", ");
|
||||||
builder.append("sessionMapperLimit=");
|
builder.append("sessionMapperLimit=");
|
||||||
builder.append(this.sessionMapperLimit);
|
builder.append(this.sessionMapperLimit);
|
||||||
|
builder.append(", ");
|
||||||
|
builder.append("ignoreDefaultFilter=");
|
||||||
|
builder.append(this.ignoreDefaultFilter);
|
||||||
|
builder.append(", ");
|
||||||
|
builder.append("performTokenExchange=");
|
||||||
|
builder.append(this.performTokenExchange);
|
||||||
|
builder.append(", ");
|
||||||
|
builder.append("alfrescoResourceName=");
|
||||||
|
builder.append(this.alfrescoResourceName);
|
||||||
builder.append("]");
|
builder.append("]");
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
@ -79,6 +79,27 @@ public class KeycloakAuthenticationConfigElementReader implements ConfigElementR
|
|||||||
configElement.setSessionMapperLimit(value.isEmpty() ? null : Integer.valueOf(value));
|
configElement.setSessionMapperLimit(value.isEmpty() ? null : Integer.valueOf(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Element ignoreDefaultFilter = element.element("ignore-default-filter");
|
||||||
|
if (ignoreDefaultFilter != null)
|
||||||
|
{
|
||||||
|
final String value = ignoreDefaultFilter.getTextTrim();
|
||||||
|
configElement.setIgnoreDefaultFilter(value.isEmpty() ? null : Boolean.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
final Element performTokenExchange = element.element("perform-token-exchange");
|
||||||
|
if (performTokenExchange != null)
|
||||||
|
{
|
||||||
|
final String value = performTokenExchange.getTextTrim();
|
||||||
|
configElement.setPerformTokenExchange(value.isEmpty() ? null : Boolean.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
final Element alfrescoResourceName = element.element("alfresco-resource-name");
|
||||||
|
if (alfrescoResourceName != null)
|
||||||
|
{
|
||||||
|
final String value = alfrescoResourceName.getTextTrim();
|
||||||
|
configElement.setAlfrescoResourceName(value.isEmpty() ? null : value);
|
||||||
|
}
|
||||||
|
|
||||||
LOGGER.debug("Read configuration element {} from XML section", configElement);
|
LOGGER.debug("Read configuration element {} from XML section", configElement);
|
||||||
|
|
||||||
return configElement;
|
return configElement;
|
||||||
|
@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 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.remote;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
import org.alfresco.web.site.servlet.SlingshotAlfrescoConnector;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.extensions.config.RemoteConfigElement.ConnectorDescriptor;
|
||||||
|
import org.springframework.extensions.surf.ServletUtil;
|
||||||
|
import org.springframework.extensions.webscripts.connector.ConnectorContext;
|
||||||
|
import org.springframework.extensions.webscripts.connector.RemoteClient;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.KeycloakSecurityContext;
|
||||||
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.OidcKeycloakAccount;
|
||||||
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.spi.KeycloakAccount;
|
||||||
|
import de.acosix.alfresco.keycloak.share.util.RefreshableAccessTokenHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class AccessTokenAwareSlingshotAlfrescoConnector extends SlingshotAlfrescoConnector
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(AccessTokenAwareSlingshotAlfrescoConnector.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class.
|
||||||
|
*
|
||||||
|
* @param descriptor
|
||||||
|
* the descriptor / configuration of this connector
|
||||||
|
* @param endpoint
|
||||||
|
* the endpoint with which this connector instance should connect
|
||||||
|
*/
|
||||||
|
public AccessTokenAwareSlingshotAlfrescoConnector(final ConnectorDescriptor descriptor, final String endpoint)
|
||||||
|
{
|
||||||
|
super(descriptor, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void applyRequestAuthentication(final RemoteClient remoteClient, final ConnectorContext context)
|
||||||
|
{
|
||||||
|
final HttpSession session = ServletUtil.getSession();
|
||||||
|
final KeycloakAccount keycloakAccount = (KeycloakAccount) (session != null ? session.getAttribute(KeycloakAccount.class.getName())
|
||||||
|
: null);
|
||||||
|
final RefreshableAccessTokenHolder accessToken = (RefreshableAccessTokenHolder) (session != null
|
||||||
|
? session.getAttribute(AccessTokenAwareSlingshotAlfrescoConnector.class.getName())
|
||||||
|
: null);
|
||||||
|
if (accessToken != null)
|
||||||
|
{
|
||||||
|
if (accessToken.isActive())
|
||||||
|
{
|
||||||
|
LOGGER.debug("Using access token for backend found in session for request");
|
||||||
|
final String tokenString = accessToken.getToken();
|
||||||
|
remoteClient.setRequestProperties(Collections.singletonMap("Authorization", "Bearer " + tokenString));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGER.warn("Acesss token for backend stored in session has expired");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (keycloakAccount instanceof OidcKeycloakAccount)
|
||||||
|
{
|
||||||
|
LOGGER.debug(
|
||||||
|
"Did not find access token for backend in session - using regularly authenticated Keycloak account access token for request instead");
|
||||||
|
final KeycloakSecurityContext keycloakSecurityContext = ((OidcKeycloakAccount) keycloakAccount).getKeycloakSecurityContext();
|
||||||
|
final String tokenString = keycloakSecurityContext.getTokenString();
|
||||||
|
remoteClient.setRequestProperties(Collections.singletonMap("Authorization", "Bearer " + tokenString));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGER.debug("Did not find Keycloak-related authentication data in session - applying regular request authentication");
|
||||||
|
super.applyRequestAuthentication(remoteClient, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,67 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 - 2020 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.remote;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
import org.alfresco.web.site.servlet.SlingshotAlfrescoConnector;
|
|
||||||
import org.springframework.extensions.config.RemoteConfigElement.ConnectorDescriptor;
|
|
||||||
import org.springframework.extensions.webscripts.connector.ConnectorContext;
|
|
||||||
import org.springframework.extensions.webscripts.connector.ConnectorSession;
|
|
||||||
import org.springframework.extensions.webscripts.connector.RemoteClient;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Axel Faust
|
|
||||||
*/
|
|
||||||
public class BearerTokenAwareSlingshotAlfrescoConnector extends SlingshotAlfrescoConnector
|
|
||||||
{
|
|
||||||
|
|
||||||
public static final String CS_PARAM_BEARER_TOKEN = "bearerToken";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new instance of this class.
|
|
||||||
*
|
|
||||||
* @param descriptor
|
|
||||||
* the descriptor / configuration of this connector
|
|
||||||
* @param endpoint
|
|
||||||
* the endpoint with which this connector instance should connect
|
|
||||||
*/
|
|
||||||
public BearerTokenAwareSlingshotAlfrescoConnector(final ConnectorDescriptor descriptor, final String endpoint)
|
|
||||||
{
|
|
||||||
super(descriptor, endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void applyRequestHeaders(final RemoteClient remoteClient, final ConnectorContext context)
|
|
||||||
{
|
|
||||||
// apply default mapping of headers
|
|
||||||
super.applyRequestHeaders(remoteClient, context);
|
|
||||||
|
|
||||||
final ConnectorSession connectorSession = this.getConnectorSession();
|
|
||||||
if (connectorSession != null)
|
|
||||||
{
|
|
||||||
final String bearerToken = connectorSession.getParameter(CS_PARAM_BEARER_TOKEN);
|
|
||||||
if (bearerToken != null && !bearerToken.trim().isEmpty())
|
|
||||||
{
|
|
||||||
remoteClient.setRequestProperties(Collections.singletonMap("Authorization", "Bearer " + bearerToken));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 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.util;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import org.alfresco.util.ParameterCheck;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.rotation.AdapterTokenVerifier.VerifiedTokens;
|
||||||
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.common.util.Time;
|
||||||
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.representations.AccessToken;
|
||||||
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.representations.AccessTokenResponse;
|
||||||
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.representations.IDToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this class encapsulate an access token with its associated refresh data.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class RefreshableAccessTokenHolder implements Serializable
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -3230026569734591820L;
|
||||||
|
|
||||||
|
protected final AccessToken accessToken;
|
||||||
|
|
||||||
|
protected final IDToken idToken;
|
||||||
|
|
||||||
|
protected final String token;
|
||||||
|
|
||||||
|
protected final String refreshToken;
|
||||||
|
|
||||||
|
protected final int refreshExpiration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class from an access token response, typically from an initial authentication or token refresh
|
||||||
|
*
|
||||||
|
* @param tokenResponse
|
||||||
|
* the response to a request for an access token
|
||||||
|
* @param verifiedTokens
|
||||||
|
* the token wrapper from the response verification step any client should do before constructing a new instance of this
|
||||||
|
* class
|
||||||
|
*/
|
||||||
|
public RefreshableAccessTokenHolder(final AccessTokenResponse tokenResponse, final VerifiedTokens verifiedTokens)
|
||||||
|
{
|
||||||
|
ParameterCheck.mandatory("tokenResponse", tokenResponse);
|
||||||
|
ParameterCheck.mandatory("verifiedTokens", verifiedTokens);
|
||||||
|
|
||||||
|
this.accessToken = verifiedTokens.getAccessToken();
|
||||||
|
this.idToken = verifiedTokens.getIdToken();
|
||||||
|
|
||||||
|
this.token = tokenResponse.getToken();
|
||||||
|
this.refreshToken = tokenResponse.getRefreshToken();
|
||||||
|
this.refreshExpiration = Time.currentTime() + (int) tokenResponse.getRefreshExpiresIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class from details exposed by Keycloak servlet adapter APIs. Since these APIs do not provide some
|
||||||
|
* access to token response details, this constructor assumes that the refresh token is valid for at least 1/100th the duration of the
|
||||||
|
* overall access token.
|
||||||
|
*
|
||||||
|
* @param accessToken
|
||||||
|
* the access token
|
||||||
|
* @param idToken
|
||||||
|
* the ID token
|
||||||
|
* @param token
|
||||||
|
* the textual representation of the access token
|
||||||
|
* @param refreshToken
|
||||||
|
* the textual representation of the refresh token
|
||||||
|
*/
|
||||||
|
public RefreshableAccessTokenHolder(final AccessToken accessToken, final IDToken idToken, final String token, final String refreshToken)
|
||||||
|
{
|
||||||
|
ParameterCheck.mandatory("accessToken", accessToken);
|
||||||
|
ParameterCheck.mandatory("idToken", idToken);
|
||||||
|
ParameterCheck.mandatoryString("token", token);
|
||||||
|
|
||||||
|
this.accessToken = accessToken;
|
||||||
|
this.idToken = idToken;
|
||||||
|
|
||||||
|
this.token = token;
|
||||||
|
this.refreshToken = refreshToken;
|
||||||
|
// no explicit refresh expiration, so assume validity period is 1/100th
|
||||||
|
this.refreshExpiration = Time.currentTime() - (accessToken.getExpiration() - Time.currentTime()) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the encapsulated access token is active.
|
||||||
|
*
|
||||||
|
* @return {@code true} if the access token is active, {@code false} otherwise
|
||||||
|
*/
|
||||||
|
public boolean isActive()
|
||||||
|
{
|
||||||
|
final boolean isActive = this.accessToken.isActive();
|
||||||
|
return isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the encapsulated access token has expired.
|
||||||
|
*
|
||||||
|
* @return {@code true} if the access token as expired, {@code false} otherwise
|
||||||
|
*/
|
||||||
|
public boolean isExpired()
|
||||||
|
{
|
||||||
|
final boolean isExpired = this.accessToken.isExpired();
|
||||||
|
return isExpired;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the encapsulated access token can be refreshed.
|
||||||
|
*
|
||||||
|
* @return {@code true} if the token can be refreshed, {@code false} otherwise
|
||||||
|
*/
|
||||||
|
public boolean canRefresh()
|
||||||
|
{
|
||||||
|
final boolean canRefresh = this.refreshToken != null && this.refreshExpiration > Time.currentTime();
|
||||||
|
return canRefresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the encapsulated access token should be refreshed.
|
||||||
|
*
|
||||||
|
* @param minTokenTTL
|
||||||
|
* the minimum time-to-live remaining before a token needs to be refreshed
|
||||||
|
*
|
||||||
|
* @return {@code true} if the token should be refreshed, {@code false} otherwise
|
||||||
|
*/
|
||||||
|
public boolean shouldRefresh(final int minTokenTTL)
|
||||||
|
{
|
||||||
|
final boolean shouldRefresh = this.refreshToken != null && this.accessToken.getExpiration() - minTokenTTL < Time.currentTime();
|
||||||
|
return shouldRefresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the token
|
||||||
|
*/
|
||||||
|
public String getToken()
|
||||||
|
{
|
||||||
|
return this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the refreshToken
|
||||||
|
*/
|
||||||
|
public String getRefreshToken()
|
||||||
|
{
|
||||||
|
return this.refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the access token
|
||||||
|
*/
|
||||||
|
public AccessToken getAccessToken()
|
||||||
|
{
|
||||||
|
return this.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the idToken
|
||||||
|
*/
|
||||||
|
public IDToken getIdToken()
|
||||||
|
{
|
||||||
|
return this.idToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -16,6 +16,7 @@
|
|||||||
package de.acosix.alfresco.keycloak.share.web;
|
package de.acosix.alfresco.keycloak.share.web;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -37,14 +38,23 @@ import javax.servlet.http.HttpServletRequestWrapper;
|
|||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import javax.servlet.http.HttpSession;
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
import org.alfresco.error.AlfrescoRuntimeException;
|
||||||
|
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.HttpHost;
|
import org.apache.http.HttpHost;
|
||||||
|
import org.apache.http.HttpResponse;
|
||||||
|
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.methods.HttpPost;
|
||||||
import org.apache.http.conn.params.ConnRoutePNames;
|
import org.apache.http.conn.params.ConnRoutePNames;
|
||||||
import org.apache.http.conn.params.ConnRouteParams;
|
import org.apache.http.conn.params.ConnRouteParams;
|
||||||
import org.apache.http.conn.routing.HttpRoute;
|
import org.apache.http.conn.routing.HttpRoute;
|
||||||
|
import org.apache.http.message.BasicNameValuePair;
|
||||||
import org.apache.http.params.HttpParams;
|
import org.apache.http.params.HttpParams;
|
||||||
|
import org.apache.http.util.EntityUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
@ -57,14 +67,12 @@ import org.springframework.extensions.surf.RequestContext;
|
|||||||
import org.springframework.extensions.surf.RequestContextUtil;
|
import org.springframework.extensions.surf.RequestContextUtil;
|
||||||
import org.springframework.extensions.surf.ServletUtil;
|
import org.springframework.extensions.surf.ServletUtil;
|
||||||
import org.springframework.extensions.surf.UserFactory;
|
import org.springframework.extensions.surf.UserFactory;
|
||||||
import org.springframework.extensions.surf.exception.ConnectorServiceException;
|
|
||||||
import org.springframework.extensions.surf.mvc.PageViewResolver;
|
import org.springframework.extensions.surf.mvc.PageViewResolver;
|
||||||
import org.springframework.extensions.surf.site.AuthenticationUtil;
|
import org.springframework.extensions.surf.site.AuthenticationUtil;
|
||||||
import org.springframework.extensions.surf.types.Page;
|
import org.springframework.extensions.surf.types.Page;
|
||||||
import org.springframework.extensions.surf.types.PageType;
|
import org.springframework.extensions.surf.types.PageType;
|
||||||
import org.springframework.extensions.webscripts.Description.RequiredAuthentication;
|
import org.springframework.extensions.webscripts.Description.RequiredAuthentication;
|
||||||
import org.springframework.extensions.webscripts.Status;
|
import org.springframework.extensions.webscripts.Status;
|
||||||
import org.springframework.extensions.webscripts.connector.Connector;
|
|
||||||
import org.springframework.extensions.webscripts.connector.ConnectorService;
|
import org.springframework.extensions.webscripts.connector.ConnectorService;
|
||||||
import org.springframework.extensions.webscripts.servlet.DependencyInjectedFilter;
|
import org.springframework.extensions.webscripts.servlet.DependencyInjectedFilter;
|
||||||
|
|
||||||
@ -72,6 +80,7 @@ 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;
|
||||||
import de.acosix.alfresco.keycloak.share.deps.keycloak.KeycloakSecurityContext;
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.KeycloakSecurityContext;
|
||||||
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.OAuth2Constants;
|
||||||
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.AdapterDeploymentContext;
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.AdapterDeploymentContext;
|
||||||
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.AuthenticatedActionsHandler;
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.AuthenticatedActionsHandler;
|
||||||
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.HttpClientBuilder;
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.HttpClientBuilder;
|
||||||
@ -80,6 +89,9 @@ import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.KeycloakDeployme
|
|||||||
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.OAuthRequestAuthenticator;
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.OAuthRequestAuthenticator;
|
||||||
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.OidcKeycloakAccount;
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.OidcKeycloakAccount;
|
||||||
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.PreAuthActionsHandler;
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.PreAuthActionsHandler;
|
||||||
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.ServerRequest;
|
||||||
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
|
||||||
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.rotation.AdapterTokenVerifier;
|
||||||
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.servlet.FilterRequestAuthenticator;
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.servlet.FilterRequestAuthenticator;
|
||||||
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.servlet.OIDCFilterSessionStore;
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.servlet.OIDCFilterSessionStore;
|
||||||
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.servlet.OIDCServletHttpFacade;
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.servlet.OIDCServletHttpFacade;
|
||||||
@ -88,9 +100,16 @@ import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.spi.Authenticati
|
|||||||
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.spi.KeycloakAccount;
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.spi.KeycloakAccount;
|
||||||
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.spi.SessionIdMapper;
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.spi.SessionIdMapper;
|
||||||
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.spi.UserSessionManagement;
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.spi.UserSessionManagement;
|
||||||
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.common.VerificationException;
|
||||||
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.common.util.KeycloakUriBuilder;
|
||||||
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.common.util.Time;
|
||||||
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.constants.ServiceUrlConstants;
|
||||||
import de.acosix.alfresco.keycloak.share.deps.keycloak.representations.AccessToken;
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.representations.AccessToken;
|
||||||
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.representations.AccessTokenResponse;
|
||||||
import de.acosix.alfresco.keycloak.share.deps.keycloak.representations.adapters.config.AdapterConfig;
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.representations.adapters.config.AdapterConfig;
|
||||||
import de.acosix.alfresco.keycloak.share.remote.BearerTokenAwareSlingshotAlfrescoConnector;
|
import de.acosix.alfresco.keycloak.share.deps.keycloak.util.JsonSerialization;
|
||||||
|
import de.acosix.alfresco.keycloak.share.remote.AccessTokenAwareSlingshotAlfrescoConnector;
|
||||||
|
import de.acosix.alfresco.keycloak.share.util.RefreshableAccessTokenHolder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keycloak-based authentication filter class which can act as a standalone filter or a facade to the default {@link SSOAuthenticationFilter
|
* Keycloak-based authentication filter class which can act as a standalone filter or a facade to the default {@link SSOAuthenticationFilter
|
||||||
@ -101,6 +120,10 @@ import de.acosix.alfresco.keycloak.share.remote.BearerTokenAwareSlingshotAlfresc
|
|||||||
public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, InitializingBean, ApplicationContextAware
|
public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, InitializingBean, ApplicationContextAware
|
||||||
{
|
{
|
||||||
|
|
||||||
|
private static final String KEYCLOAK_ACCOUNT_SESSION_KEY = KeycloakAccount.class.getName();
|
||||||
|
|
||||||
|
private static final String BACKEND_ACCESS_TOKEN_SESSION_KEY = AccessTokenAwareSlingshotAlfrescoConnector.class.getName();
|
||||||
|
|
||||||
private static final String HEADER_AUTHORIZATION = "Authorization";
|
private static final String HEADER_AUTHORIZATION = "Authorization";
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAuthenticationFilter.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAuthenticationFilter.class);
|
||||||
@ -149,6 +172,8 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
|
|||||||
|
|
||||||
protected boolean forceSso = false;
|
protected boolean forceSso = false;
|
||||||
|
|
||||||
|
protected boolean ignoreDefaultFilter = false;
|
||||||
|
|
||||||
protected KeycloakDeployment keycloakDeployment;
|
protected KeycloakDeployment keycloakDeployment;
|
||||||
|
|
||||||
protected AdapterDeploymentContext deploymentContext;
|
protected AdapterDeploymentContext deploymentContext;
|
||||||
@ -178,7 +203,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
|
|||||||
{
|
{
|
||||||
final HttpSession currentSession = req.getSession(false);
|
final HttpSession currentSession = req.getSession(false);
|
||||||
authenticatedByKeycloak = currentSession != null && AuthenticationUtil.isAuthenticated(req)
|
authenticatedByKeycloak = currentSession != null && AuthenticationUtil.isAuthenticated(req)
|
||||||
&& currentSession.getAttribute(KeycloakAccount.class.getName()) != null;
|
&& currentSession.getAttribute(KEYCLOAK_ACCOUNT_SESSION_KEY) != null;
|
||||||
}
|
}
|
||||||
return authenticatedByKeycloak;
|
return authenticatedByKeycloak;
|
||||||
}
|
}
|
||||||
@ -286,6 +311,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.ignoreDefaultFilter = Boolean.TRUE.equals(keycloakAuthConfig.getIgnoreDefaultFilter());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -375,17 +401,26 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
|
|||||||
final HttpServletResponse res = (HttpServletResponse) response;
|
final HttpServletResponse res = (HttpServletResponse) response;
|
||||||
LOGGER.debug("Entered doFilter for {}", req);
|
LOGGER.debug("Entered doFilter for {}", req);
|
||||||
|
|
||||||
if (this.isLogoutRequest(req))
|
final KeycloakUriBuilder authUrl = this.keycloakDeployment.getAuthUrl();
|
||||||
|
final boolean keycloakDeploymentReady = authUrl != null;
|
||||||
|
if (!keycloakDeploymentReady)
|
||||||
|
{
|
||||||
|
LOGGER.warn("Cannot process Keycloak-specifics as Keycloak library was unable to resolve relative URLs from {}",
|
||||||
|
this.keycloakDeployment.getAuthServerBaseUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keycloakDeploymentReady && this.isLogoutRequest(req))
|
||||||
{
|
{
|
||||||
this.processLogout(context, req, res, chain);
|
this.processLogout(context, req, res, chain);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
final boolean skip = this.checkForSkipCondition(req, res);
|
final boolean skip = !keycloakDeploymentReady || this.checkForSkipCondition(req, res);
|
||||||
|
|
||||||
if (skip)
|
if (skip)
|
||||||
{
|
{
|
||||||
if (!AuthenticationUtil.isAuthenticated(req) && this.loginFormEnhancementEnabled && this.isLoginPage(req))
|
if (keycloakDeploymentReady && !AuthenticationUtil.isAuthenticated(req) && this.loginFormEnhancementEnabled
|
||||||
|
&& this.isLoginPage(req))
|
||||||
{
|
{
|
||||||
this.prepareLoginFormEnhancement(context, req, res);
|
this.prepareLoginFormEnhancement(context, req, res);
|
||||||
}
|
}
|
||||||
@ -414,7 +449,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
|
|||||||
final HttpSession currentSession = req.getSession(false);
|
final HttpSession currentSession = req.getSession(false);
|
||||||
|
|
||||||
if (currentSession != null && AuthenticationUtil.isAuthenticated(req)
|
if (currentSession != null && AuthenticationUtil.isAuthenticated(req)
|
||||||
&& currentSession.getAttribute(KeycloakAccount.class.getName()) != null
|
&& currentSession.getAttribute(KEYCLOAK_ACCOUNT_SESSION_KEY) != null
|
||||||
&& this.sessionIdMapper.hasSession(currentSession.getId()))
|
&& this.sessionIdMapper.hasSession(currentSession.getId()))
|
||||||
{
|
{
|
||||||
LOGGER.debug("Processing logout for Keycloak-authenticated user {} in session {}", AuthenticationUtil.getUserId(req),
|
LOGGER.debug("Processing logout for Keycloak-authenticated user {} in session {}", AuthenticationUtil.getUserId(req),
|
||||||
@ -676,7 +711,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
|
|||||||
final OIDCFilterSessionStore tokenStore) throws IOException, ServletException
|
final OIDCFilterSessionStore tokenStore) throws IOException, ServletException
|
||||||
{
|
{
|
||||||
final HttpSession session = req.getSession();
|
final HttpSession session = req.getSession();
|
||||||
final Object keycloakAccount = session != null ? session.getAttribute(KeycloakAccount.class.getName()) : null;
|
final Object keycloakAccount = session != null ? session.getAttribute(KEYCLOAK_ACCOUNT_SESSION_KEY) : null;
|
||||||
if (keycloakAccount instanceof OidcKeycloakAccount)
|
if (keycloakAccount instanceof OidcKeycloakAccount)
|
||||||
{
|
{
|
||||||
final KeycloakSecurityContext keycloakSecurityContext = ((OidcKeycloakAccount) keycloakAccount).getKeycloakSecurityContext();
|
final KeycloakSecurityContext keycloakSecurityContext = ((OidcKeycloakAccount) keycloakAccount).getKeycloakSecurityContext();
|
||||||
@ -684,17 +719,10 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
|
|||||||
final String userId = accessToken.getPreferredUsername();
|
final String userId = accessToken.getPreferredUsername();
|
||||||
LOGGER.debug("User {} successfully authenticated via Keycloak", userId);
|
LOGGER.debug("User {} successfully authenticated via Keycloak", userId);
|
||||||
|
|
||||||
final String accessTokenString = keycloakSecurityContext.getTokenString();
|
|
||||||
this.updateEndpointConnectorBearerToken(this.primaryEndpoint, userId, session, accessTokenString);
|
|
||||||
if (this.secondaryEndpoints != null)
|
|
||||||
{
|
|
||||||
this.secondaryEndpoints.forEach(endpoint -> {
|
|
||||||
this.updateEndpointConnectorBearerToken(endpoint, userId, session, accessTokenString);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
|
this.handleAlfrescoResourceAccessToken(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (facade.isEnded())
|
if (facade.isEnded())
|
||||||
@ -771,10 +799,10 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
|
|||||||
final FilterChain chain) throws IOException, ServletException
|
final FilterChain chain) throws IOException, ServletException
|
||||||
{
|
{
|
||||||
final HttpSession session = ((HttpServletRequest) request).getSession(false);
|
final HttpSession session = ((HttpServletRequest) request).getSession(false);
|
||||||
final Object keycloakAccount = session != null ? session.getAttribute(KeycloakAccount.class.getName()) : null;
|
final Object keycloakAccount = session != null ? session.getAttribute(KEYCLOAK_ACCOUNT_SESSION_KEY) : null;
|
||||||
|
|
||||||
// no point in forwarding to default SSO filter if already authenticated
|
// no point in forwarding to default SSO filter if already authenticated
|
||||||
if (this.defaultSsoFilter != null && keycloakAccount == null)
|
if (this.defaultSsoFilter != null && keycloakAccount == null && !this.ignoreDefaultFilter)
|
||||||
{
|
{
|
||||||
this.defaultSsoFilter.doFilter(context, request, response, chain);
|
this.defaultSsoFilter.doFilter(context, request, response, chain);
|
||||||
}
|
}
|
||||||
@ -812,7 +840,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
|
|||||||
|
|
||||||
// check for back-channel logout (sessionIdMapper should now of all authenticated sessions)
|
// check for back-channel logout (sessionIdMapper should now of all authenticated sessions)
|
||||||
if (this.externalAuthEnabled && this.filterEnabled && this.keycloakDeployment != null && currentSession != null
|
if (this.externalAuthEnabled && this.filterEnabled && this.keycloakDeployment != null && currentSession != null
|
||||||
&& AuthenticationUtil.isAuthenticated(req) && currentSession.getAttribute(KeycloakAccount.class.getName()) != null
|
&& AuthenticationUtil.isAuthenticated(req) && currentSession.getAttribute(KEYCLOAK_ACCOUNT_SESSION_KEY) != null
|
||||||
&& !this.sessionIdMapper.hasSession(currentSession.getId()))
|
&& !this.sessionIdMapper.hasSession(currentSession.getId()))
|
||||||
{
|
{
|
||||||
LOGGER.debug("Session {} for Keycloak-authenticated user {} was invalidated by back-channel logout", currentSession.getId(),
|
LOGGER.debug("Session {} for Keycloak-authenticated user {} was invalidated by back-channel logout", currentSession.getId(),
|
||||||
@ -855,7 +883,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
|
|||||||
}
|
}
|
||||||
else if (currentSession != null && AuthenticationUtil.isAuthenticated(req))
|
else if (currentSession != null && AuthenticationUtil.isAuthenticated(req))
|
||||||
{
|
{
|
||||||
final KeycloakAccount keycloakAccount = (KeycloakAccount) currentSession.getAttribute(KeycloakAccount.class.getName());
|
final KeycloakAccount keycloakAccount = (KeycloakAccount) currentSession.getAttribute(KEYCLOAK_ACCOUNT_SESSION_KEY);
|
||||||
if (keycloakAccount != null)
|
if (keycloakAccount != null)
|
||||||
{
|
{
|
||||||
skip = this.validateAndRefreshKeycloakAuthentication(req, res, AuthenticationUtil.getUserId(req), keycloakAccount);
|
skip = this.validateAndRefreshKeycloakAuthentication(req, res, AuthenticationUtil.getUserId(req), keycloakAccount);
|
||||||
@ -937,24 +965,8 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
|
|||||||
if (currentSession != null)
|
if (currentSession != null)
|
||||||
{
|
{
|
||||||
LOGGER.debug("Skipping processKeycloakAuthenticationAndActions as Keycloak-authentication session is still valid");
|
LOGGER.debug("Skipping processKeycloakAuthenticationAndActions as Keycloak-authentication session is still valid");
|
||||||
|
this.handleAlfrescoResourceAccessToken(currentSession);
|
||||||
skip = true;
|
skip = true;
|
||||||
|
|
||||||
if (keycloakAccount instanceof OidcKeycloakAccount)
|
|
||||||
{
|
|
||||||
final KeycloakSecurityContext keycloakSecurityContext = ((OidcKeycloakAccount) keycloakAccount)
|
|
||||||
.getKeycloakSecurityContext();
|
|
||||||
|
|
||||||
final String accessTokenString = keycloakSecurityContext.getTokenString();
|
|
||||||
|
|
||||||
final HttpSession effectiveSession = currentSession;
|
|
||||||
this.updateEndpointConnectorBearerToken(this.primaryEndpoint, userId, effectiveSession, accessTokenString);
|
|
||||||
if (this.secondaryEndpoints != null)
|
|
||||||
{
|
|
||||||
this.secondaryEndpoints.forEach(endpoint -> {
|
|
||||||
this.updateEndpointConnectorBearerToken(endpoint, userId, effectiveSession, accessTokenString);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -1089,20 +1101,6 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
|
|||||||
return isLogoutRequest;
|
return isLogoutRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void updateEndpointConnectorBearerToken(final String endpoint, final String userId, final HttpSession session,
|
|
||||||
final String tokenString)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
final Connector conn = this.connectorService.getConnector(endpoint, userId, session);
|
|
||||||
conn.getConnectorSession().setParameter(BearerTokenAwareSlingshotAlfrescoConnector.CS_PARAM_BEARER_TOKEN, tokenString);
|
|
||||||
}
|
|
||||||
catch (final ConnectorServiceException e)
|
|
||||||
{
|
|
||||||
LOGGER.warn("Endpoint {} has not been defined", endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the HTTP request has set the Keycloak state cookie.
|
* Checks if the HTTP request has set the Keycloak state cookie.
|
||||||
*
|
*
|
||||||
@ -1120,6 +1118,176 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
|
|||||||
return hasStateCookie;
|
return hasStateCookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks, initialises and/or refreshes the access token for accessing the Alfresco backend based on configuration and current session
|
||||||
|
* state / validity of any existing token.
|
||||||
|
*
|
||||||
|
* @param session
|
||||||
|
* the active session managing any persistent access token state
|
||||||
|
*/
|
||||||
|
protected void handleAlfrescoResourceAccessToken(final HttpSession session)
|
||||||
|
{
|
||||||
|
final KeycloakAuthenticationConfigElement keycloakAuthConfig = (KeycloakAuthenticationConfigElement) this.configService
|
||||||
|
.getConfig(KeycloakConfigConstants.KEYCLOAK_CONFIG_SECTION_NAME).getConfigElement(KeycloakAuthenticationConfigElement.NAME);
|
||||||
|
if (keycloakAuthConfig != null && Boolean.TRUE.equals(keycloakAuthConfig.getPerformTokenExchange()))
|
||||||
|
{
|
||||||
|
final String alfrescoResourceName = keycloakAuthConfig.getAlfrescoResourceName();
|
||||||
|
if (!EqualsHelper.nullSafeEquals(alfrescoResourceName, this.keycloakDeployment.getResourceName())
|
||||||
|
&& alfrescoResourceName != null)
|
||||||
|
{
|
||||||
|
final Object backendAccessTokenCandidate = session.getAttribute(BACKEND_ACCESS_TOKEN_SESSION_KEY);
|
||||||
|
RefreshableAccessTokenHolder token;
|
||||||
|
if (!(backendAccessTokenCandidate instanceof RefreshableAccessTokenHolder))
|
||||||
|
{
|
||||||
|
LOGGER.debug("Session does not yet contain an access token for the Alfresco backend resource {}", alfrescoResourceName);
|
||||||
|
token = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
token = (RefreshableAccessTokenHolder) backendAccessTokenCandidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// not really feasible to synchronise / lock concurrent refresh on token
|
||||||
|
// not a big problem - apart from wasted CPU cycles / latency - since each concurrently refreshed token is valid
|
||||||
|
// independently
|
||||||
|
if (token == null || (token.canRefresh() && token.shouldRefresh(this.keycloakDeployment.getTokenMinimumTimeToLive())))
|
||||||
|
{
|
||||||
|
AccessTokenResponse response;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (token != null)
|
||||||
|
{
|
||||||
|
LOGGER.debug("Refreshing access token for Alfresco backend resource {}", alfrescoResourceName);
|
||||||
|
response = ServerRequest.invokeRefresh(this.keycloakDeployment, token.getRefreshToken());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGER.debug("Retrieving initial access token for Alfresco backend resource {}", alfrescoResourceName);
|
||||||
|
response = this.getAccessToken(alfrescoResourceName, session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (final IOException ioex)
|
||||||
|
{
|
||||||
|
LOGGER.error("Error retrieving / refreshing access token for Alfresco backend", ioex);
|
||||||
|
throw new AlfrescoRuntimeException("Error retrieving / refreshing access token for Alfresco backend", ioex);
|
||||||
|
}
|
||||||
|
catch (final ServerRequest.HttpFailure httpFailure)
|
||||||
|
{
|
||||||
|
LOGGER.error("Refreshing access token for Alfresco backend failed: {} {}", httpFailure.getStatus(),
|
||||||
|
httpFailure.getError());
|
||||||
|
throw new AlfrescoRuntimeException("Failed to refresh access token for Alfresco backend: " + httpFailure.getStatus()
|
||||||
|
+ " " + httpFailure.getError());
|
||||||
|
}
|
||||||
|
|
||||||
|
final String tokenString = response.getToken();
|
||||||
|
final AdapterTokenVerifier.VerifiedTokens tokens;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
tokens = AdapterTokenVerifier.verifyTokens(tokenString, response.getIdToken(), this.keycloakDeployment);
|
||||||
|
}
|
||||||
|
catch (final VerificationException vex)
|
||||||
|
{
|
||||||
|
LOGGER.error("Verification of access token for Alfresco backend failed", vex);
|
||||||
|
throw new AlfrescoRuntimeException("Failed to verify access token for Alfresco backend", vex);
|
||||||
|
}
|
||||||
|
|
||||||
|
final AccessToken accessToken = tokens.getAccessToken();
|
||||||
|
|
||||||
|
if ((accessToken.getExpiration() - this.keycloakDeployment.getTokenMinimumTimeToLive()) <= Time.currentTime())
|
||||||
|
{
|
||||||
|
throw new AlfrescoRuntimeException(
|
||||||
|
"Failed to retrieve / refresh the access token for the Alfresco backend with a longer time-to-live than the minimum");
|
||||||
|
}
|
||||||
|
|
||||||
|
token = new RefreshableAccessTokenHolder(response, tokens);
|
||||||
|
session.setAttribute(BACKEND_ACCESS_TOKEN_SESSION_KEY, token);
|
||||||
|
LOGGER.debug("Successfully retrieved / refresh access token for Alfresco backend");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (alfrescoResourceName == null)
|
||||||
|
{
|
||||||
|
LOGGER.warn(
|
||||||
|
"Encountered configuration error: alfresco-resource-name has not been set, which is required for performing token exchange");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGER.warn(
|
||||||
|
"Encountered configuration error: alfresco-resource-name is set to the same value as Share's adapter resource, which is unsuitable for performing token exchange");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGER.debug("Use of token exchange has not been enabled - calls to Alfresco backend will reuse the global access token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains an access token for the Alfresco backend by exchanging the current user access token in the session for an access token to
|
||||||
|
* that backend resource.
|
||||||
|
*
|
||||||
|
* @param alfrescoResourceName
|
||||||
|
* the name of the Alfresco backend resource within the Keycloak realm
|
||||||
|
* @param session
|
||||||
|
* the active session managing any persistent access token state
|
||||||
|
* @return the response to obtaining the access token for the Alfresco backend
|
||||||
|
*/
|
||||||
|
protected AccessTokenResponse getAccessToken(final String alfrescoResourceName, final HttpSession session) throws IOException
|
||||||
|
{
|
||||||
|
AccessTokenResponse tokenResponse = null;
|
||||||
|
final HttpClient client = this.keycloakDeployment.getClient();
|
||||||
|
|
||||||
|
final HttpPost post = new HttpPost(KeycloakUriBuilder.fromUri(this.keycloakDeployment.getAuthServerBaseUrl())
|
||||||
|
.path(ServiceUrlConstants.TOKEN_PATH).build(this.keycloakDeployment.getRealm()));
|
||||||
|
final List<NameValuePair> formParams = new ArrayList<>();
|
||||||
|
|
||||||
|
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE));
|
||||||
|
formParams.add(new BasicNameValuePair(OAuth2Constants.AUDIENCE, alfrescoResourceName));
|
||||||
|
formParams.add(new BasicNameValuePair(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.REFRESH_TOKEN_TYPE));
|
||||||
|
|
||||||
|
final OidcKeycloakAccount keycloakAccount = (OidcKeycloakAccount) session.getAttribute(KEYCLOAK_ACCOUNT_SESSION_KEY);
|
||||||
|
final String tokenString = keycloakAccount.getKeycloakSecurityContext().getTokenString();
|
||||||
|
formParams.add(new BasicNameValuePair(OAuth2Constants.SUBJECT_TOKEN, tokenString));
|
||||||
|
|
||||||
|
ClientCredentialsProviderUtils.setClientCredentials(this.keycloakDeployment, post, formParams);
|
||||||
|
|
||||||
|
final UrlEncodedFormEntity form = new UrlEncodedFormEntity(formParams, "UTF-8");
|
||||||
|
post.setEntity(form);
|
||||||
|
|
||||||
|
final HttpResponse response = client.execute(post);
|
||||||
|
final int status = response.getStatusLine().getStatusCode();
|
||||||
|
final HttpEntity entity = response.getEntity();
|
||||||
|
if (status != 200)
|
||||||
|
{
|
||||||
|
final String statusReason = response.getStatusLine().getReasonPhrase();
|
||||||
|
LOGGER.debug("Failed to retrieve access token due to HTTP {}: {}", status, statusReason);
|
||||||
|
EntityUtils.consumeQuietly(entity);
|
||||||
|
throw new AlfrescoRuntimeException("Failed to retrieve access token due to HTTP error " + status + ": " + statusReason);
|
||||||
|
}
|
||||||
|
if (entity == null)
|
||||||
|
{
|
||||||
|
throw new AlfrescoRuntimeException("Response to access token request did not contain a response body");
|
||||||
|
}
|
||||||
|
|
||||||
|
final InputStream is = entity.getContent();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
tokenResponse = JsonSerialization.readValue(is, AccessTokenResponse.class);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
is.close();
|
||||||
|
}
|
||||||
|
catch (final IOException e)
|
||||||
|
{
|
||||||
|
LOGGER.trace("Error closing entity stream", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenResponse;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets any Keycloak-related state cookies present in the current request.
|
* Resets any Keycloak-related state cookies present in the current request.
|
||||||
*
|
*
|
||||||
@ -1147,6 +1315,16 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a forced route for the Keycloak-library backing HTTP client if configured. This may be necessary to deal with situations
|
||||||
|
* where Share cannot use the public address of the authentication server (used in authentication redirects) to talk with the server
|
||||||
|
* directly, due to network isolation / addressing restrictions (e.g. in Docker-ized deployments).
|
||||||
|
*
|
||||||
|
* @param configElement
|
||||||
|
* the adapter configuration
|
||||||
|
* @param client
|
||||||
|
* the client to configure
|
||||||
|
*/
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
protected void configureForcedRouteIfNecessary(final KeycloakAdapterConfigElement configElement, final HttpClient client)
|
protected void configureForcedRouteIfNecessary(final KeycloakAdapterConfigElement configElement, final HttpClient client)
|
||||||
{
|
{
|
||||||
|
@ -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.authentication.directAuthHost=http://host.docker.internal:8380
|
keycloak.authentication.directAuthHost=http://keycloak:8080
|
||||||
|
|
||||||
keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=/Test A
|
keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=/Test A
|
||||||
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=/Test A
|
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=/Test A
|
||||||
|
@ -21,8 +21,8 @@
|
|||||||
<connector>
|
<connector>
|
||||||
<id>alfrescoCookie</id>
|
<id>alfrescoCookie</id>
|
||||||
<name>Alfresco Connector</name>
|
<name>Alfresco Connector</name>
|
||||||
<description>Connects to an Alfresco instance using cookie-based authentication and awareness of OIDC bearer tokens</description>
|
<description>Connects to an Alfresco instance using cookie-based authentication and awareness of Keycloak access tokens</description>
|
||||||
<class>de.acosix.alfresco.keycloak.share.remote.BearerTokenAwareSlingshotAlfrescoConnector</class>
|
<class>de.acosix.alfresco.keycloak.share.remote.AccessTokenAwareSlingshotAlfrescoConnector</class>
|
||||||
</connector>
|
</connector>
|
||||||
|
|
||||||
<endpoint>
|
<endpoint>
|
||||||
@ -64,9 +64,10 @@
|
|||||||
<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>false</force-keycloak-sso>
|
||||||
|
<perform-token-exchange>true</perform-token-exchange>
|
||||||
</keycloak-auth-config>
|
</keycloak-auth-config>
|
||||||
<keycloak-adapter-config>
|
<keycloak-adapter-config>
|
||||||
<directAuthHost>http://host.docker.internal:8380</directAuthHost>
|
<directAuthHost>http://keycloak:8080</directAuthHost>
|
||||||
<auth-server-url>http://${docker.tests.host.name}:${docker.tests.keycloakPort}/auth</auth-server-url>
|
<auth-server-url>http://${docker.tests.host.name}:${docker.tests.keycloakPort}/auth</auth-server-url>
|
||||||
<realm>test</realm>
|
<realm>test</realm>
|
||||||
<resource>alfresco-share</resource>
|
<resource>alfresco-share</resource>
|
||||||
|
1
share/src/test/docker/keycloakProfile.properties
Normal file
1
share/src/test/docker/keycloakProfile.properties
Normal file
@ -0,0 +1 @@
|
|||||||
|
feature.token_exchange=enabled
|
@ -14,8 +14,7 @@
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
<assembly
|
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
|
||||||
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
|
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
|
||||||
<id>share-it-docker</id>
|
<id>share-it-docker</id>
|
||||||
@ -74,6 +73,7 @@
|
|||||||
<dependencySet>
|
<dependencySet>
|
||||||
<outputDirectory>WEB-INF/lib</outputDirectory>
|
<outputDirectory>WEB-INF/lib</outputDirectory>
|
||||||
<includes>
|
<includes>
|
||||||
|
<include>org.slf4j:slf4j-log4j12:*</include>
|
||||||
<include>org.orderofthebee.support-tools:support-tools-share:*</include>
|
<include>org.orderofthebee.support-tools:support-tools-share:*</include>
|
||||||
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.common:*</include>
|
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.common:*</include>
|
||||||
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.share:jar:installable:*</include>
|
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.share:jar:installable:*</include>
|
||||||
|
@ -30,7 +30,8 @@
|
|||||||
"serviceAccountsEnabled": true,
|
"serviceAccountsEnabled": true,
|
||||||
"publicClient": false,
|
"publicClient": false,
|
||||||
"protocol": "openid-connect"
|
"protocol": "openid-connect"
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
"id": "alfresco-share",
|
"id": "alfresco-share",
|
||||||
"clientId": "alfresco-share",
|
"clientId": "alfresco-share",
|
||||||
"name": "Alfresco Share",
|
"name": "Alfresco Share",
|
||||||
@ -47,6 +48,158 @@
|
|||||||
"secret": "a5b3e8bc-39cc-4ddd-8c8f-1c34e7a35975",
|
"secret": "a5b3e8bc-39cc-4ddd-8c8f-1c34e7a35975",
|
||||||
"publicClient": false,
|
"publicClient": false,
|
||||||
"protocol": "openid-connect"
|
"protocol": "openid-connect"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId": "realm-management",
|
||||||
|
"name": "Realm Management",
|
||||||
|
"enabled": true,
|
||||||
|
"clientAuthenticatorType": "client-secret",
|
||||||
|
"bearerOnly": true,
|
||||||
|
"authorizationServicesEnabled": true,
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"authorizationSettings": {
|
||||||
|
"allowRemoteResourceManagement": false,
|
||||||
|
"policyEnforcementMode": "ENFORCING",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"name": "client.resource.alfresco",
|
||||||
|
"type": "Client",
|
||||||
|
"ownerManagedAccess": false,
|
||||||
|
"attributes": {
|
||||||
|
|
||||||
|
},
|
||||||
|
"uris": [],
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "view"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "map-roles-client-scope"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "configure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "map-roles"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "manage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "token-exchange"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "map-roles-composite"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"name": "alfresco-token-exchange",
|
||||||
|
"type": "client",
|
||||||
|
"logic": "POSITIVE",
|
||||||
|
"decisionStrategy": "UNANIMOUS",
|
||||||
|
"config": {
|
||||||
|
"clients": "[\"alfresco-share\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "token-exchange.permission.client.alfresco",
|
||||||
|
"type": "scope",
|
||||||
|
"logic": "POSITIVE",
|
||||||
|
"decisionStrategy": "UNANIMOUS",
|
||||||
|
"config": {
|
||||||
|
"resources": "[\"client.resource.alfresco\"]",
|
||||||
|
"scopes": "[\"token-exchange\"]",
|
||||||
|
"applyPolicies": "[\"alfresco-token-exchange\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "map-roles-composite.permission.client.alfresco",
|
||||||
|
"type": "scope",
|
||||||
|
"logic": "POSITIVE",
|
||||||
|
"decisionStrategy": "UNANIMOUS",
|
||||||
|
"config": {
|
||||||
|
"resources": "[\"client.resource.alfresco\"]",
|
||||||
|
"scopes": "[\"map-roles-composite\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "map-roles-client-scope.permission.client.alfresco",
|
||||||
|
"type": "scope",
|
||||||
|
"logic": "POSITIVE",
|
||||||
|
"decisionStrategy": "UNANIMOUS",
|
||||||
|
"config": {
|
||||||
|
"resources": "[\"client.resource.alfresco\"]",
|
||||||
|
"scopes": "[\"map-roles-client-scope\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "map-roles.permission.client.alfresco",
|
||||||
|
"type": "scope",
|
||||||
|
"logic": "POSITIVE",
|
||||||
|
"decisionStrategy": "UNANIMOUS",
|
||||||
|
"config": {
|
||||||
|
"resources": "[\"client.resource.alfresco\"]",
|
||||||
|
"scopes": "[\"map-roles\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "view.permission.client.alfresco",
|
||||||
|
"type": "scope",
|
||||||
|
"logic": "POSITIVE",
|
||||||
|
"decisionStrategy": "UNANIMOUS",
|
||||||
|
"config": {
|
||||||
|
"resources": "[\"client.resource.alfresco\"]",
|
||||||
|
"scopes": "[\"view\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "configure.permission.client.alfresco",
|
||||||
|
"type": "scope",
|
||||||
|
"logic": "POSITIVE",
|
||||||
|
"decisionStrategy": "UNANIMOUS",
|
||||||
|
"config": {
|
||||||
|
"resources": "[\"client.resource.alfresco\"]",
|
||||||
|
"scopes": "[\"configure\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "manage.permission.client.alfresco",
|
||||||
|
"type": "scope",
|
||||||
|
"logic": "POSITIVE",
|
||||||
|
"decisionStrategy": "UNANIMOUS",
|
||||||
|
"config": {
|
||||||
|
"resources": "[\"client.resource.alfresco\"]",
|
||||||
|
"scopes": "[\"manage\"]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "token-exchange"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "configure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "map-roles-composite"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "map-roles-client-scope"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "map-roles"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "view"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "manage"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"roles": {
|
"roles": {
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
</element-readers>
|
</element-readers>
|
||||||
</plug-ins>
|
</plug-ins>
|
||||||
|
|
||||||
<!-- sensible default configuration (similar to Repository identity-service-authentication.properties -->
|
<!-- sensible default configuration -->
|
||||||
<config evaluator="string-compare" condition="Keycloak">
|
<config evaluator="string-compare" condition="Keycloak">
|
||||||
<keycloak-auth-config>
|
<keycloak-auth-config>
|
||||||
<enhance-login-form>true</enhance-login-form>
|
<enhance-login-form>true</enhance-login-form>
|
||||||
@ -35,8 +35,12 @@
|
|||||||
<ssl-redirect-port>8443</ssl-redirect-port>
|
<ssl-redirect-port>8443</ssl-redirect-port>
|
||||||
<body-buffer-limit>10485760</body-buffer-limit>
|
<body-buffer-limit>10485760</body-buffer-limit>
|
||||||
<session-mapper-limit>1000</session-mapper-limit>
|
<session-mapper-limit>1000</session-mapper-limit>
|
||||||
|
<ignore-default-filter>true</ignore-default-filter>
|
||||||
|
<perform-token-exchange>false</perform-token-exchange>
|
||||||
|
<alfresco-resource-name>alfresco</alfresco-resource-name>
|
||||||
</keycloak-auth-config>
|
</keycloak-auth-config>
|
||||||
<keycloak-adapter-config>
|
<keycloak-adapter-config>
|
||||||
|
<!-- 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>
|
||||||
<resource>alfresco</resource>
|
<resource>alfresco</resource>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user