Custom Bearer handling in Share

This commit is contained in:
AFaust
2020-03-08 20:12:32 +01:00
parent 0deb5ee8a8
commit f8bdd8ce43
3 changed files with 223 additions and 19 deletions

View File

@@ -31,6 +31,7 @@ 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;
import de.acosix.alfresco.keycloak.share.web.KeycloakAuthenticationFilter;
/**
* @author Axel Faust
@@ -61,17 +62,21 @@ public class AccessTokenAwareSlingshotAlfrescoConnector extends SlingshotAlfresc
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())
final KeycloakAccount keycloakAccount = (KeycloakAccount) (session != null
? session.getAttribute(KeycloakAuthenticationFilter.KEYCLOAK_ACCOUNT_SESSION_KEY)
: null);
final RefreshableAccessTokenHolder accessToken = (RefreshableAccessTokenHolder) (session != null
? session.getAttribute(AccessTokenAwareSlingshotAlfrescoConnector.class.getName())
? session.getAttribute(KeycloakAuthenticationFilter.ACCESS_TOKEN_SESSION_KEY)
: null);
if (accessToken != null)
final RefreshableAccessTokenHolder endpointSpecificAccessToken = (RefreshableAccessTokenHolder) (session != null
? session.getAttribute(KeycloakAuthenticationFilter.BACKEND_ACCESS_TOKEN_SESSION_KEY)
: null);
if (endpointSpecificAccessToken != null)
{
if (accessToken.isActive())
if (endpointSpecificAccessToken.isActive())
{
LOGGER.debug("Using access token for backend found in session for request");
final String tokenString = accessToken.getToken();
final String tokenString = endpointSpecificAccessToken.getToken();
remoteClient.setRequestProperties(Collections.singletonMap("Authorization", "Bearer " + tokenString));
}
else
@@ -87,6 +92,13 @@ public class AccessTokenAwareSlingshotAlfrescoConnector extends SlingshotAlfresc
final String tokenString = keycloakSecurityContext.getTokenString();
remoteClient.setRequestProperties(Collections.singletonMap("Authorization", "Bearer " + tokenString));
}
else if (accessToken != null)
{
LOGGER.debug(
"Did not find access token for backend in session - using Bearer access token provided in original authentication request for request instead");
final String tokenString = accessToken.getToken();
remoteClient.setRequestProperties(Collections.singletonMap("Authorization", "Bearer " + tokenString));
}
else
{
LOGGER.debug("Did not find Keycloak-related authentication data in session - applying regular request authentication");

View File

@@ -26,7 +26,7 @@ import de.acosix.alfresco.keycloak.share.deps.keycloak.representations.AccessTok
import de.acosix.alfresco.keycloak.share.deps.keycloak.representations.IDToken;
/**
* Instances of this class encapsulate an access token with its associated refresh data.
* Instances of this class encapsulate a potentially refreshable access token.
*
* @author Axel Faust
*/

View File

@@ -85,6 +85,7 @@ 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.AuthenticatedActionsHandler;
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.BearerTokenRequestAuthenticator;
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.HttpClientBuilder;
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.KeycloakDeployment;
import de.acosix.alfresco.keycloak.share.deps.keycloak.adapters.KeycloakDeploymentBuilder;
@@ -122,9 +123,11 @@ import de.acosix.alfresco.keycloak.share.util.RefreshableAccessTokenHolder;
public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, InitializingBean, ApplicationContextAware
{
private static final String KEYCLOAK_ACCOUNT_SESSION_KEY = KeycloakAccount.class.getName();
public static final String KEYCLOAK_ACCOUNT_SESSION_KEY = KeycloakAccount.class.getName();
private static final String BACKEND_ACCESS_TOKEN_SESSION_KEY = AccessTokenAwareSlingshotAlfrescoConnector.class.getName();
public static final String ACCESS_TOKEN_SESSION_KEY = AccessToken.class.getName();
public static final String BACKEND_ACCESS_TOKEN_SESSION_KEY = AccessTokenAwareSlingshotAlfrescoConnector.class.getName();
private static final String HEADER_AUTHORIZATION = "Authorization";
@@ -543,6 +546,111 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
}
}
final String authHeader = req.getHeader(HEADER_AUTHORIZATION);
if (authHeader != null && authHeader.toLowerCase(Locale.ENGLISH).startsWith("bearer "))
{
this.processBearerAuthentication(context, req, res, chain, keycloakAuthConfig.getPerformTokenExchange(), facade);
}
else
{
this.processFilterAuthentication(context, req, res, chain, bodyBufferLimit, sslRedirectPort, facade);
}
}
/**
* Processes authentication when an explicit "Bearer" authentication header is present in a request. Such authentication is only
* supported when Share is not using OAuth2 token exchange with the Repository backend, and requires a bit of special handling due to
* Keycloak library access restrictions, in order to obtain the access token for validation and passing on to the Repository-tier.
*
* @param context
* the servlet context
* @param req
* the servlet request
* @param res
* the servlet response
* @param chain
* the filter chain
* @param performTokenExchange
* whether Share has been configured to perform OAuth2 token exchange to authenticate against the Repository backend
* @param facade
* the Keycloak HTTP facade
* @throws IOException
* if any error occurs during Keycloak authentication or processing of the filter chain
* @throws ServletException
* if any error occurs during Keycloak authentication or processing of the filter chain
*/
protected void processBearerAuthentication(final ServletContext context, final HttpServletRequest req, final HttpServletResponse res,
final FilterChain chain, final Boolean performTokenExchange, final OIDCServletHttpFacade facade)
throws IOException, ServletException
{
if (Boolean.TRUE.equals(performTokenExchange))
{
LOGGER.warn(
"Authentication was attempted using Bearer token - this cannot be supported using token exchange for accessing the primary backend endpoint {}",
this.primaryEndpoint);
LOGGER.warn("Continueing with filter chain processing without handling the Bearer token");
this.continueFilterChain(context, req, res, chain);
}
else
{
final BearerTokenRequestAuthenticator authenticator = new BearerTokenRequestAuthenticator(this.keycloakDeployment);
final AuthOutcome authOutcome = authenticator.authenticate(facade);
if (authOutcome == AuthOutcome.AUTHENTICATED)
{
final AccessToken token = authenticator.getToken();
final RefreshableAccessTokenHolder tokenHolder = new RefreshableAccessTokenHolder(token, token,
authenticator.getTokenString(), null);
this.onKeycloakAuthenticationSuccess(context, req, res, chain, facade, tokenHolder);
}
else if (authOutcome == AuthOutcome.FAILED)
{
LOGGER.warn("Bearer token authentication failed - issueing failure challenge with details");
// not using regular onKeycloakAuthenticationFailure handling since that only applies to proper OIDC filter authentication,
// with the potential of redirecting to the IdP authentication UI
req.getSession().invalidate();
authenticator.getChallenge().challenge(facade);
}
else
{
LOGGER.warn(
"Unexpected authentication outcome {} on Bearer authentication with guaranteed token presence - continueing with filter chain processing",
authOutcome);
this.continueFilterChain(context, req, res, chain);
}
}
}
/**
* Processes the regular OIDC filter authentication on a request.
*
* @param context
* the servlet context
* @param req
* the servlet request
* @param res
* the servlet response
* @param chain
* the filter chain
* @param bodyBufferLimit
* the configured size limit to apply to any HTTP POST/PUT body buffering that may need to be applied to process the
* authentication via an intermediary redirect
* @param sslRedirectPort
* the configured port to use for any forced redirection to HTTPS/SSL communication
* @param facade
* the Keycloak HTTP facade
* @throws IOException
* if any error occurs during Keycloak authentication or processing of the filter chain
* @throws ServletException
* if any error occurs during Keycloak authentication or processing of the filter chain
*/
protected void processFilterAuthentication(final ServletContext context, final HttpServletRequest req, final HttpServletResponse res,
final FilterChain chain, final Integer bodyBufferLimit, final Integer sslRedirectPort, final OIDCServletHttpFacade facade)
throws IOException, ServletException
{
final OIDCFilterSessionStore tokenStore = new OIDCFilterSessionStore(req, facade,
bodyBufferLimit != null ? bodyBufferLimit.intValue() : DEFAULT_BODY_BUFFER_LIMIT, this.keycloakDeployment,
this.sessionIdMapper);
@@ -689,7 +797,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
}
/**
* Processes a sucessfull authentication via Keycloak.
* Processes a successful authentication via Keycloak.
*
* @param context
* the servlet context
@@ -752,6 +860,64 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
this.continueFilterChain(context, requestWrapper, res, chain);
}
/**
* Processes a successful authentication via Keycloak.
*
* @param context
* the servlet context
* @param req
* the servlet request
* @param res
* the servlet response
* @param chain
* the filter chain
* @param facade
* the Keycloak HTTP facade
* @param tokenHolder
* the holder for access token taken from the successful authentication
* @throws IOException
* if any error occurs during Keycloak authentication or processing of the filter chain
* @throws ServletException
* if any error occurs during Keycloak authentication or processing of the filter chain
*/
protected void onKeycloakAuthenticationSuccess(final ServletContext context, final HttpServletRequest req,
final HttpServletResponse res, final FilterChain chain, final OIDCServletHttpFacade facade,
final RefreshableAccessTokenHolder tokenHolder) throws IOException, ServletException
{
final HttpSession session = req.getSession();
session.setAttribute(ACCESS_TOKEN_SESSION_KEY, tokenHolder);
final String userId = tokenHolder.getAccessToken().getPreferredUsername();
LOGGER.debug("User {} successfully authenticated via Keycloak", userId);
session.setAttribute(UserFactory.SESSION_ATTRIBUTE_EXTERNAL_AUTH, Boolean.TRUE);
session.setAttribute(UserFactory.SESSION_ATTRIBUTE_KEY_USER_ID, userId);
if (facade.isEnded())
{
LOGGER.debug("Authenticator already handled response");
return;
}
final String servletPath = req.getServletPath();
final String pathInfo = req.getPathInfo();
final String servletRequestUri = servletPath + (pathInfo != null ? pathInfo : "");
if (servletRequestUri.matches(KEYCLOAK_ACTION_URL_PATTERN))
{
LOGGER.debug("Applying Keycloak authenticated actions handler");
final AuthenticatedActionsHandler actions = new AuthenticatedActionsHandler(this.keycloakDeployment, facade);
if (actions.handledRequest())
{
LOGGER.debug("Keycloak authenticated actions processed the request - stopping filter chain execution");
return;
}
}
LOGGER.debug("Continueing with filter chain processing");
this.continueFilterChain(context, req, res, chain);
}
/**
* Processes a failed authentication via Keycloak.
*
@@ -858,13 +1024,29 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
// check for back-channel logout (sessionIdMapper should now of all authenticated sessions)
if (this.externalAuthEnabled && this.filterEnabled && this.keycloakDeployment != null && currentSession != null
&& AuthenticationUtil.isAuthenticated(req) && currentSession.getAttribute(KEYCLOAK_ACCOUNT_SESSION_KEY) != null
&& !this.sessionIdMapper.hasSession(currentSession.getId()))
&& AuthenticationUtil.isAuthenticated(req))
{
LOGGER.debug("Session {} for Keycloak-authenticated user {} was invalidated by back-channel logout", currentSession.getId(),
AuthenticationUtil.getUserId(req));
currentSession.invalidate();
currentSession = req.getSession(false);
if (currentSession.getAttribute(KEYCLOAK_ACCOUNT_SESSION_KEY) != null
&& !this.sessionIdMapper.hasSession(currentSession.getId()))
{
LOGGER.debug("Session {} for Keycloak-authenticated user {} was invalidated by back-channel logout", currentSession.getId(),
AuthenticationUtil.getUserId(req));
currentSession.invalidate();
currentSession = req.getSession(false);
}
else if (currentSession.getAttribute(ACCESS_TOKEN_SESSION_KEY) != null)
{
final RefreshableAccessTokenHolder accessToken = (RefreshableAccessTokenHolder) currentSession
.getAttribute(ACCESS_TOKEN_SESSION_KEY);
if (!accessToken.isActive())
{
LOGGER.debug("Access token in session from previous Bearer authorization for {} has expired - invalidating session",
AuthenticationUtil.getUserId(req));
currentSession.invalidate();
currentSession = req.getSession(false);
}
}
}
if (!this.externalAuthEnabled || !this.filterEnabled)
@@ -888,7 +1070,8 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
}
else if (authHeader != null && authHeader.toLowerCase(Locale.ENGLISH).startsWith("bearer "))
{
LOGGER.debug("Explicitly not skipping processKeycloakAuthenticationAndActions as Bearer authorization header is present");
LOGGER.debug(
"Explicitly not skipping processKeycloakAuthenticationAndActions as Bearer authorization header is present and Bearer authentication is not disallowed");
}
else if (authHeader != null && authHeader.toLowerCase(Locale.ENGLISH).startsWith("basic "))
{
@@ -922,16 +1105,24 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
final String noauth = proxyMatcher.group(2);
if (noauth != null && !noauth.trim().isEmpty())
{
LOGGER.debug("Skipping processKeycloakAuthenticationAndActions as proxy servlet to noauth endpoint {} is being called");
LOGGER.debug("Skipping processKeycloakAuthenticationAndActions as proxy servlet to noauth endpoint {} is being called",
endpoint);
skip = true;
}
else if (!endpoint.equals(this.primaryEndpoint)
&& (this.secondaryEndpoints == null || !this.secondaryEndpoints.contains(endpoint)))
{
LOGGER.debug(
"Skipping processKeycloakAuthenticationAndActions on proxy servlet call as endpoint {} has not been configured as a primary / secondary endpoint to handle");
"Skipping processKeycloakAuthenticationAndActions on proxy servlet call as endpoint {} has not been configured as a primary / secondary endpoint to handle",
endpoint);
skip = true;
}
else
{
LOGGER.debug(
"Explicitely not skipping processKeycloakAuthenticationAndActions on proxy servlet call to endpoint {} which is expected to serve web scripts requiring authentication",
endpoint);
}
}
else if (PAGE_SERVLET_PATH.equals(servletPath) && (LOGIN_PATH_INFORMATION.equals(pathInfo)
|| (pathInfo == null && LOGIN_PAGE_TYPE_PARAMETER_VALUE.equals(req.getParameter(PAGE_TYPE_PARAMETER_NAME)))))
@@ -1168,7 +1359,8 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
// 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.isActive() || (token.canRefresh() && token.shouldRefresh(this.keycloakDeployment.getTokenMinimumTimeToLive())))
if (token == null || !token.isActive()
|| (token.canRefresh() && token.shouldRefresh(this.keycloakDeployment.getTokenMinimumTimeToLive())))
{
AccessTokenResponse response;
try