Test and deal with granular client scopes

This commit is contained in:
AFaust 2021-10-18 01:36:12 +02:00
parent 5933acbb04
commit cff32d017b
13 changed files with 1449 additions and 240 deletions

View File

@ -162,6 +162,7 @@
<property name="accessTokenService" ref="accessTokenService.impl" /> <property name="accessTokenService" ref="accessTokenService.impl" />
<property name="userName" value="${keycloak.synchronization.user}" /> <property name="userName" value="${keycloak.synchronization.user}" />
<property name="password" value="${keycloak.synchronization.password}" /> <property name="password" value="${keycloak.synchronization.password}" />
<property name="requiredClientScopes" value="${keycloak.synchronization.requiredClientScopes}" />
</bean> </bean>
<bean id="userRegistry" class="${project.artifactId}.sync.KeycloakUserRegistry"> <bean id="userRegistry" class="${project.artifactId}.sync.KeycloakUserRegistry">

View File

@ -97,6 +97,7 @@ keycloak.roles.resourceMapper.default.prefix.property.prefix=ROLE_KEYCLOAK_${key
keycloak.synchronization.enabled=true keycloak.synchronization.enabled=true
keycloak.synchronization.user= keycloak.synchronization.user=
keycloak.synchronization.password= keycloak.synchronization.password=
keycloak.synchronization.requiredClientScopes=
keycloak.synchronization.personLoadBatchSize=50 keycloak.synchronization.personLoadBatchSize=50
keycloak.synchronization.groupLoadBatchSize=50 keycloak.synchronization.groupLoadBatchSize=50

View File

@ -120,7 +120,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
/** /**
* @param active * @param active
* the active to set * the active to set
*/ */
public void setActive(final boolean active) public void setActive(final boolean active)
{ {
@ -138,7 +138,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
/** /**
* @param allowUserNamePasswordLogin * @param allowUserNamePasswordLogin
* the allowUserNamePasswordLogin to set * the allowUserNamePasswordLogin to set
*/ */
public void setAllowUserNamePasswordLogin(final boolean allowUserNamePasswordLogin) public void setAllowUserNamePasswordLogin(final boolean allowUserNamePasswordLogin)
{ {
@ -147,7 +147,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
/** /**
* @param failExpiredTicketTokens * @param failExpiredTicketTokens
* the failExpiredTicketTokens to set * the failExpiredTicketTokens to set
*/ */
public void setFailExpiredTicketTokens(final boolean failExpiredTicketTokens) public void setFailExpiredTicketTokens(final boolean failExpiredTicketTokens)
{ {
@ -156,7 +156,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
/** /**
* @param allowGuestLogin * @param allowGuestLogin
* the allowGuestLogin to set * the allowGuestLogin to set
*/ */
public void setAllowGuestLogin(final boolean allowGuestLogin) public void setAllowGuestLogin(final boolean allowGuestLogin)
{ {
@ -166,7 +166,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
/** /**
* @param allowGuestLogin * @param allowGuestLogin
* the allowGuestLogin to set * the allowGuestLogin to set
*/ */
@Override @Override
public void setAllowGuestLogin(final Boolean allowGuestLogin) public void setAllowGuestLogin(final Boolean allowGuestLogin)
@ -176,7 +176,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
/** /**
* @param mapAuthorities * @param mapAuthorities
* the mapAuthorities to set * the mapAuthorities to set
*/ */
public void setMapAuthorities(final boolean mapAuthorities) public void setMapAuthorities(final boolean mapAuthorities)
{ {
@ -185,7 +185,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
/** /**
* @param mapPersonPropertiesOnLogin * @param mapPersonPropertiesOnLogin
* the mapPersonPropertiesOnLogin to set * the mapPersonPropertiesOnLogin to set
*/ */
public void setMapPersonPropertiesOnLogin(final boolean mapPersonPropertiesOnLogin) public void setMapPersonPropertiesOnLogin(final boolean mapPersonPropertiesOnLogin)
{ {
@ -194,7 +194,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
/** /**
* @param deployment * @param deployment
* the deployment to set * the deployment to set
*/ */
public void setDeployment(final KeycloakDeployment deployment) public void setDeployment(final KeycloakDeployment deployment)
{ {
@ -237,9 +237,9 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
* expired and the component has been configured to not accept expired tokens. * expired and the component has been configured to not accept expired tokens.
* *
* @param ticketToken * @param ticketToken
* the refreshable access token to refresh * the refreshable access token to refresh
* @return the refreshed access token if a refresh was possible AND necessary, and a new access token has been retrieved from Keycloak - * @return the refreshed access token if a refresh was possible AND necessary, and a new access token has been retrieved from Keycloak -
* will be {@code null} if no refresh has taken place * will be {@code null} if no refresh has taken place
*/ */
public RefreshableAccessTokenHolder checkAndRefreshTicketToken(final RefreshableAccessTokenHolder ticketToken) public RefreshableAccessTokenHolder checkAndRefreshTicketToken(final RefreshableAccessTokenHolder ticketToken)
throws AuthenticationException throws AuthenticationException
@ -288,7 +288,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
String realUserName = userName; String realUserName = userName;
try try
{ {
accessTokenHolder = this.accessTokenClient.obtainAccessToken(userName, new String(password)); accessTokenHolder = this.accessTokenClient.obtainAccessToken(userName, new String(password), Collections.emptySet());
realUserName = accessTokenHolder.getAccessToken().getPreferredUsername(); realUserName = accessTokenHolder.getAccessToken().getPreferredUsername();
// for potential one-off authentication, we do not care particularly about the token TTL - so no validation here // for potential one-off authentication, we do not care particularly about the token TTL - so no validation here
@ -315,13 +315,13 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
* instance. * instance.
* *
* @param accessToken * @param accessToken
* the access token * the access token
* @param idToken * @param idToken
* the ID token * the ID token
* @param freshLogin * @param freshLogin
* {@code true} if the tokens are fresh, that is have just been obtained from an initial login, {@code false} otherwise - * {@code true} if the tokens are fresh, that is have just been obtained from an initial login, {@code false} otherwise -
* Alfresco person node properties will only be mapped for fresh tokens, while granted authorities processors will always be * Alfresco person node properties will only be mapped for fresh tokens, while granted authorities processors will always be
* handled if enabled * handled if enabled
*/ */
public void handleUserTokens(final AccessToken accessToken, final IDToken idToken, final boolean freshLogin) public void handleUserTokens(final AccessToken accessToken, final IDToken idToken, final boolean freshLogin)
{ {
@ -372,9 +372,9 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
* Updates the person for the current user with data mapped from the Keycloak tokens. * Updates the person for the current user with data mapped from the Keycloak tokens.
* *
* @param accessToken * @param accessToken
* the access token * the access token
* @param idToken * @param idToken
* the ID token * the ID token
*/ */
protected void updatePerson(final AccessToken accessToken, final IDToken idToken) protected void updatePerson(final AccessToken accessToken, final IDToken idToken)
{ {

View File

@ -21,6 +21,9 @@ import com.fasterxml.jackson.databind.MappingIterator;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -65,6 +68,8 @@ public class IDMClientImpl implements InitializingBean, IDMClient
protected String password; protected String password;
protected final Collection<String> requiredClientScopes = new HashSet<>();
protected AccessTokenHolder accessToken; protected AccessTokenHolder accessToken;
/** /**
@ -79,7 +84,7 @@ public class IDMClientImpl implements InitializingBean, IDMClient
/** /**
* @param deployment * @param deployment
* the deployment to set * the deployment to set
*/ */
public void setDeployment(final KeycloakDeployment deployment) public void setDeployment(final KeycloakDeployment deployment)
{ {
@ -88,7 +93,7 @@ public class IDMClientImpl implements InitializingBean, IDMClient
/** /**
* @param accessTokenService * @param accessTokenService
* the accessTokenService to set * the accessTokenService to set
*/ */
public void setAccessTokenService(final AccessTokenService accessTokenService) public void setAccessTokenService(final AccessTokenService accessTokenService)
{ {
@ -97,7 +102,7 @@ public class IDMClientImpl implements InitializingBean, IDMClient
/** /**
* @param userName * @param userName
* the userName to set * the userName to set
*/ */
public void setUserName(final String userName) public void setUserName(final String userName)
{ {
@ -106,13 +111,26 @@ public class IDMClientImpl implements InitializingBean, IDMClient
/** /**
* @param password * @param password
* the password to set * the password to set
*/ */
public void setPassword(final String password) public void setPassword(final String password)
{ {
this.password = password; this.password = password;
} }
/**
* @param requiredClientScopes
* the requiredClientScopes to set
*/
public void setRequiredClientScopes(final String requiredClientScopes)
{
this.requiredClientScopes.clear();
if (requiredClientScopes != null && !requiredClientScopes.isEmpty())
{
this.requiredClientScopes.addAll(Arrays.asList(requiredClientScopes.trim().split(" ")));
}
}
/** /**
* *
* {@inheritDoc} * {@inheritDoc}
@ -414,13 +432,13 @@ public class IDMClientImpl implements InitializingBean, IDMClient
* Loads and processes a batch of generic entities from Keycloak. * Loads and processes a batch of generic entities from Keycloak.
* *
* @param <T> * @param <T>
* the type of the response entities * the type of the response entities
* @param uri * @param uri
* the URI to call * the URI to call
* @param entityProcessor * @param entityProcessor
* the processor handling the loaded entities * the processor handling the loaded entities
* @param entityClass * @param entityClass
* the type of the expected response entities * the type of the expected response entities
* @return the number of processed entities * @return the number of processed entities
*/ */
protected <T> int processEntityBatch(final URI uri, final Consumer<T> entityProcessor, final Class<T> entityClass) protected <T> int processEntityBatch(final URI uri, final Consumer<T> entityProcessor, final Class<T> entityClass)
@ -483,9 +501,9 @@ public class IDMClientImpl implements InitializingBean, IDMClient
* Executes a generic HTTP GET operation yielding a JSON response. * Executes a generic HTTP GET operation yielding a JSON response.
* *
* @param uri * @param uri
* the URI to call * the URI to call
* @param responseProcessor * @param responseProcessor
* the processor handling the response JSON * the processor handling the response JSON
*/ */
protected void processGenericGet(final URI uri, final Consumer<JsonNode> responseProcessor) protected void processGenericGet(final URI uri, final Consumer<JsonNode> responseProcessor)
{ {
@ -539,11 +557,11 @@ public class IDMClientImpl implements InitializingBean, IDMClient
* Executes a generic HTTP GET operation yielding a mapped response entity. * Executes a generic HTTP GET operation yielding a mapped response entity.
* *
* @param <T> * @param <T>
* the type of the response entity * the type of the response entity
* @param uri * @param uri
* the URI to call * the URI to call
* @param responseType * @param responseType
* the class object for the type of the response entity * the class object for the type of the response entity
* @return the response entity * @return the response entity
* *
*/ */
@ -610,11 +628,12 @@ public class IDMClientImpl implements InitializingBean, IDMClient
{ {
if (this.userName != null && !this.userName.isEmpty()) if (this.userName != null && !this.userName.isEmpty())
{ {
this.accessToken = this.accessTokenService.obtainAccessToken(this.userName, this.password); this.accessToken = this.accessTokenService.obtainAccessToken(this.userName, this.password,
this.requiredClientScopes);
} }
else else
{ {
this.accessToken = this.accessTokenService.obtainAccessToken(); this.accessToken = this.accessTokenService.obtainAccessToken(this.requiredClientScopes);
} }
} }
} }

View File

@ -3,6 +3,7 @@ package de.acosix.alfresco.keycloak.repo.token;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -57,17 +58,21 @@ public class AccessTokenClient
* Obtains an access token for the service account of the client used to integrate this Alfresco isntance with Keycloak. This requires * Obtains an access token for the service account of the client used to integrate this Alfresco isntance with Keycloak. This requires
* that a service account has been enabled / configured in Keycloak. * that a service account has been enabled / configured in Keycloak.
* *
* @param scopes
* the optional scopes to request for the access token
* @return the access token * @return the access token
* @throws AccessTokenException * @throws AccessTokenException
* if the access token cannot be obtained * if the access token cannot be obtained
*/ */
public RefreshableAccessTokenHolder obtainAccessToken() public RefreshableAccessTokenHolder obtainAccessToken(final Collection<String> scopes)
{ {
LOGGER.debug("Obtaining client access token"); ParameterCheck.mandatory("scopes", scopes);
LOGGER.debug("Obtaining client access token with (optional) scopes {}", scopes);
try try
{ {
final AccessTokenResponse response = this.getAccessTokenImpl(formParams -> { final AccessTokenResponse response = this.getAccessTokenImpl(formParams -> {
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
this.processScopes(scopes, formParams);
}); });
final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response); final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response);
final RefreshableAccessTokenHolder refreshableToken = new RefreshableAccessTokenHolder(response, verifiedTokens); final RefreshableAccessTokenHolder refreshableToken = new RefreshableAccessTokenHolder(response, verifiedTokens);
@ -85,22 +90,29 @@ public class AccessTokenClient
* Alfresco instance with Keycloak is configured to allow direct access grants. * Alfresco instance with Keycloak is configured to allow direct access grants.
* *
* @param user * @param user
* the name of the user * the name of the user
* @param password * @param password
* the password provided by / for the user * the password provided by / for the user
* @param scopes
* the optional scopes to request for the access token
* @return the access token * @return the access token
* @throws AccessTokenException * @throws AccessTokenException
* if the access token cannot be obtained * if the access token cannot be obtained
*/ */
public RefreshableAccessTokenHolder obtainAccessToken(final String user, final String password) public RefreshableAccessTokenHolder obtainAccessToken(final String user, final String password, final Collection<String> scopes)
{ {
LOGGER.debug("Obtaining access token for user {}", user); ParameterCheck.mandatoryString("user", user);
ParameterCheck.mandatoryString("password", password);
ParameterCheck.mandatory("scopes", scopes);
LOGGER.debug("Obtaining access token for user {} with (optional) scopes {}", user, scopes);
try try
{ {
final AccessTokenResponse response = this.getAccessTokenImpl(formParams -> { final AccessTokenResponse response = this.getAccessTokenImpl(formParams -> {
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD)); formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
formParams.add(new BasicNameValuePair("username", user)); formParams.add(new BasicNameValuePair("username", user));
formParams.add(new BasicNameValuePair("password", password)); formParams.add(new BasicNameValuePair("password", password));
this.processScopes(scopes, formParams);
}); });
final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response); final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response);
final RefreshableAccessTokenHolder refreshableToken = new RefreshableAccessTokenHolder(response, verifiedTokens); final RefreshableAccessTokenHolder refreshableToken = new RefreshableAccessTokenHolder(response, verifiedTokens);
@ -118,22 +130,29 @@ public class AccessTokenClient
* the original identity but enabling that client / service to know the access is being delegated through this Alfresco instance. * the original identity but enabling that client / service to know the access is being delegated through this Alfresco instance.
* *
* @param accessToken * @param accessToken
* the access token to exchange * the access token to exchange
* @param client * @param client
* the client / service for which to obtain an access token * the client / service for which to obtain an access token
* @param scopes
* the optional scopes to request for the access token
* @return the access token to the requested client / service * @return the access token to the requested client / service
* @throws AccessTokenException * @throws AccessTokenException
* if the token cannot be exchanged * if the token cannot be exchanged
*/ */
public RefreshableAccessTokenHolder exchangeToken(final String accessToken, final String client) public RefreshableAccessTokenHolder exchangeToken(final String accessToken, final String client, final Collection<String> scopes)
{ {
LOGGER.debug("Exchanging {} for access token to client {}", accessToken, client); ParameterCheck.mandatoryString("accessToken", accessToken);
ParameterCheck.mandatoryString("client", client);
ParameterCheck.mandatory("scopes", scopes);
LOGGER.debug("Exchanging {} for access token to client {} with (optional) scopes {}", accessToken, client, scopes);
try try
{ {
final AccessTokenResponse response = this.getAccessTokenImpl(formParams -> { final AccessTokenResponse response = this.getAccessTokenImpl(formParams -> {
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)); formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE));
formParams.add(new BasicNameValuePair(OAuth2Constants.AUDIENCE, client)); formParams.add(new BasicNameValuePair(OAuth2Constants.AUDIENCE, client));
formParams.add(new BasicNameValuePair(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.REFRESH_TOKEN_TYPE)); formParams.add(new BasicNameValuePair(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.REFRESH_TOKEN_TYPE));
this.processScopes(scopes, formParams);
}); });
final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response, client); final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response, client);
final RefreshableAccessTokenHolder refreshableToken = new RefreshableAccessTokenHolder(response, verifiedTokens); final RefreshableAccessTokenHolder refreshableToken = new RefreshableAccessTokenHolder(response, verifiedTokens);
@ -150,10 +169,10 @@ public class AccessTokenClient
* Refreshes an access token via a previously obtained refresh token. * Refreshes an access token via a previously obtained refresh token.
* *
* @param refreshToken * @param refreshToken
* the refresh token with which to retrieve a fresh access token * the refresh token with which to retrieve a fresh access token
* @return the fresh access token * @return the fresh access token
* @throws AccessTokenRefreshException * @throws AccessTokenRefreshException
* if the refresh failed * if the refresh failed
*/ */
public RefreshableAccessTokenHolder refreshAccessToken(final String refreshToken) public RefreshableAccessTokenHolder refreshAccessToken(final String refreshToken)
{ {
@ -178,6 +197,23 @@ public class AccessTokenClient
} }
} }
protected void processScopes(final Collection<String> scopes, final List<NameValuePair> formParams)
{
if (!scopes.isEmpty())
{
final StringBuilder sb = new StringBuilder(scopes.size() * 16);
for (final String scope : scopes)
{
if (sb.length() > 0)
{
sb.append(' ');
}
sb.append(scope);
}
formParams.add(new BasicNameValuePair(OAuth2Constants.SCOPE, sb.toString()));
}
}
protected VerifiedTokens verifyAccessTokenResponse(final AccessTokenResponse response) protected VerifiedTokens verifyAccessTokenResponse(final AccessTokenResponse response)
{ {
final VerifiedTokens tokens; final VerifiedTokens tokens;
@ -229,10 +265,10 @@ public class AccessTokenClient
* Retrieves an OIDC access token with the specific token request parameter up to the caller to define via the provided consumer. * Retrieves an OIDC access token with the specific token request parameter up to the caller to define via the provided consumer.
* *
* @param postParamProvider * @param postParamProvider
* a provider of HTTP POST parameters for the access token request * a provider of HTTP POST parameters for the access token request
* @return the access token * @return the access token
* @throws IOException * @throws IOException
* when errors occur in the HTTP interaction * when errors occur in the HTTP interaction
*/ */
// implementing this method locally avoids having the dependency on Keycloak authz-client // implementing this method locally avoids having the dependency on Keycloak authz-client
// authz-client does not support refresh, so would be of limited value anyway // authz-client does not support refresh, so would be of limited value anyway

View File

@ -15,6 +15,9 @@
*/ */
package de.acosix.alfresco.keycloak.repo.token; package de.acosix.alfresco.keycloak.repo.token;
import java.util.Collection;
import java.util.Collections;
/** /**
* Instances of this interface allow for the retrieval of access tokens in the Keycloak realm to which this Alfresco instance is connected. * Instances of this interface allow for the retrieval of access tokens in the Keycloak realm to which this Alfresco instance is connected.
* *
@ -28,35 +31,85 @@ public interface AccessTokenService
* *
* @return the holder for the access token * @return the holder for the access token
* @throws IllegalStateException * @throws IllegalStateException
* if no access token for the client can be obtained, e.g. if no service account for it has been configured in Keycloak * if no access token for the client can be obtained, e.g. if no service account for it has been configured in Keycloak
*/ */
AccessTokenHolder obtainAccessToken(); default AccessTokenHolder obtainAccessToken()
{
return this.obtainAccessToken(Collections.emptySet());
}
/**
* Obtains a generic realm access token for the specific client of this Alfresco instance.
*
* @param scopes
* the optional scopes to request for the access token
* @return the holder for the access token
* @throws IllegalStateException
* if no access token for the client can be obtained, e.g. if no service account for it has been configured in Keycloak
*/
AccessTokenHolder obtainAccessToken(Collection<String> scopes);
/** /**
* Obtains a generic realm access token for a specific user of the realm. * Obtains a generic realm access token for a specific user of the realm.
* *
* @param user * @param user
* the name of the user * the name of the user
* @param password * @param password
* the password of the user * the password of the user
* @return the holder for the access token * @return the holder for the access token
* @throws IllegalStateException * @throws IllegalStateException
* if no access token for the client can be obtained, e.g. if direct access grants have not been enabled for the specific * if no access token for the user can be obtained
* client of this Alfresco instance
*/ */
AccessTokenHolder obtainAccessToken(String user, String password); default AccessTokenHolder obtainAccessToken(final String user, final String password)
{
return this.obtainAccessToken(user, password, Collections.emptySet());
}
/**
* Obtains a generic realm access token for a specific user of the realm.
*
* @param user
* the name of the user
* @param password
* the password of the user
* @param scopes
* the optional scopes to request for the access token
* @return the holder for the access token
* @throws IllegalStateException
* if no access token for the user can be obtained
*/
AccessTokenHolder obtainAccessToken(String user, String password, Collection<String> scopes);
/** /**
* Performs a token exchange operation to obtain an access token for a specific client, acting in the name / context of the user for * Performs a token exchange operation to obtain an access token for a specific client, acting in the name / context of the user for
* which the original access token was issued. * which the original access token was issued.
* *
* @param accessToken * @param accessToken
* the access token to exchange for token to another client * the access token to exchange for token to another client
* @param client * @param client
* the client for which to obtain an access token * the client for which to obtain an access token
* @return the holder for the access token * @return the holder for the access token
* @throws IllegalStateException * @throws IllegalStateException
* if no access token for the client can be obtained, e.g. if no service account for it has been configured in Keycloak * if the access token cannot be exchanged for the client
*/ */
AccessTokenHolder exchangeToken(String accessToken, String client); default AccessTokenHolder exchangeToken(final String accessToken, final String client)
{
return this.exchangeToken(accessToken, client, Collections.emptySet());
}
/**
* Performs a token exchange operation to obtain an access token for a specific client, acting in the name / context of the user for
* which the original access token was issued.
*
* @param accessToken
* the access token to exchange for token to another client
* @param client
* the client for which to obtain an access token
* @param scopes
* the optional scopes to request for the access token
* @return the holder for the access token
* @throws IllegalStateException
* if the access token cannot be exchanged for the client
*/
AccessTokenHolder exchangeToken(String accessToken, String client, Collection<String> scopes);
} }

View File

@ -15,6 +15,8 @@
*/ */
package de.acosix.alfresco.keycloak.repo.token; package de.acosix.alfresco.keycloak.repo.token;
import java.util.Collection;
import org.alfresco.util.ParameterCheck; import org.alfresco.util.ParameterCheck;
import org.alfresco.util.PropertyCheck; import org.alfresco.util.PropertyCheck;
import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeployment;
@ -47,7 +49,7 @@ public class AccessTokenServiceImpl implements AccessTokenService, InitializingB
/** /**
* @param deployment * @param deployment
* the deployment to set * the deployment to set
*/ */
public void setDeployment(final KeycloakDeployment deployment) public void setDeployment(final KeycloakDeployment deployment)
{ {
@ -59,15 +61,16 @@ public class AccessTokenServiceImpl implements AccessTokenService, InitializingB
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public AccessTokenHolder obtainAccessToken() public AccessTokenHolder obtainAccessToken(final Collection<String> scopes)
{ {
final RefreshableAccessTokenHolder refreshableToken = this.accessTokenClient.obtainAccessToken(); ParameterCheck.mandatory("scopes", scopes);
final RefreshableAccessTokenHolder refreshableToken = this.accessTokenClient.obtainAccessToken(scopes);
return new AccessTokenHolderImpl(refreshableToken, this.deployment.getTokenMinimumTimeToLive(), return new AccessTokenHolderImpl(refreshableToken, this.deployment.getTokenMinimumTimeToLive(),
this.accessTokenClient::refreshAccessToken, () -> { this.accessTokenClient::refreshAccessToken, () -> {
try try
{ {
return this.accessTokenClient.obtainAccessToken(); return this.accessTokenClient.obtainAccessToken(scopes);
} }
catch (final AccessTokenException atex) catch (final AccessTokenException atex)
{ {
@ -81,18 +84,18 @@ public class AccessTokenServiceImpl implements AccessTokenService, InitializingB
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public AccessTokenHolder obtainAccessToken(final String user, final String password) public AccessTokenHolder obtainAccessToken(final String user, final String password, final Collection<String> scopes)
{ {
ParameterCheck.mandatoryString("user", user); ParameterCheck.mandatoryString("user", user);
ParameterCheck.mandatoryString("password", password); ParameterCheck.mandatoryString("password", password);
final RefreshableAccessTokenHolder refreshableToken = this.accessTokenClient.obtainAccessToken(user, password); final RefreshableAccessTokenHolder refreshableToken = this.accessTokenClient.obtainAccessToken(user, password, scopes);
return new AccessTokenHolderImpl(refreshableToken, this.deployment.getTokenMinimumTimeToLive(), return new AccessTokenHolderImpl(refreshableToken, this.deployment.getTokenMinimumTimeToLive(),
this.accessTokenClient::refreshAccessToken, () -> { this.accessTokenClient::refreshAccessToken, () -> {
try try
{ {
return this.accessTokenClient.obtainAccessToken(user, password); return this.accessTokenClient.obtainAccessToken(user, password, scopes);
} }
catch (final AccessTokenException atex) catch (final AccessTokenException atex)
{ {
@ -105,18 +108,18 @@ public class AccessTokenServiceImpl implements AccessTokenService, InitializingB
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public AccessTokenHolder exchangeToken(final String accessToken, final String client) public AccessTokenHolder exchangeToken(final String accessToken, final String client, final Collection<String> scopes)
{ {
ParameterCheck.mandatoryString("accessToken", accessToken); ParameterCheck.mandatoryString("accessToken", accessToken);
ParameterCheck.mandatoryString("client", client); ParameterCheck.mandatoryString("client", client);
final RefreshableAccessTokenHolder refreshableToken = this.accessTokenClient.exchangeToken(accessToken, client); final RefreshableAccessTokenHolder refreshableToken = this.accessTokenClient.exchangeToken(accessToken, client, scopes);
return new AccessTokenHolderImpl(refreshableToken, this.deployment.getTokenMinimumTimeToLive(), refreshToken -> { return new AccessTokenHolderImpl(refreshableToken, this.deployment.getTokenMinimumTimeToLive(), refreshToken -> {
try try
{ {
final String newAccessToken = this.accessTokenClient.refreshAccessToken(refreshToken).getToken(); final String newAccessToken = this.accessTokenClient.refreshAccessToken(refreshToken).getToken();
return this.accessTokenClient.exchangeToken(newAccessToken, client); return this.accessTokenClient.exchangeToken(newAccessToken, client, scopes);
} }
catch (final AccessTokenException atex) catch (final AccessTokenException atex)
{ {

View File

@ -15,6 +15,8 @@
*/ */
package de.acosix.alfresco.keycloak.repo.token; package de.acosix.alfresco.keycloak.repo.token;
import java.util.Collection;
/** /**
* This no-op implementation class of an access token service may be used as a default implemenation in a subsystem proxy to avoid failing * This no-op implementation class of an access token service may be used as a default implemenation in a subsystem proxy to avoid failing
* if no Keycloak subsystem instance is active. * if no Keycloak subsystem instance is active.
@ -31,7 +33,7 @@ public class NoOpAccessTokenServiceImpl implements AccessTokenService
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public AccessTokenHolder obtainAccessToken() public AccessTokenHolder obtainAccessToken(final Collection<String> scopes)
{ {
throw new AccessTokenUnsupportedException(UNSUPPORTED_MESSAGE); throw new AccessTokenUnsupportedException(UNSUPPORTED_MESSAGE);
} }
@ -41,7 +43,7 @@ public class NoOpAccessTokenServiceImpl implements AccessTokenService
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public AccessTokenHolder obtainAccessToken(final String user, final String password) public AccessTokenHolder obtainAccessToken(final String user, final String password, final Collection<String> scopes)
{ {
throw new AccessTokenUnsupportedException(UNSUPPORTED_MESSAGE); throw new AccessTokenUnsupportedException(UNSUPPORTED_MESSAGE);
} }
@ -51,7 +53,7 @@ public class NoOpAccessTokenServiceImpl implements AccessTokenService
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public AccessTokenHolder exchangeToken(final String accessToken, final String client) public AccessTokenHolder exchangeToken(final String accessToken, final String client, final Collection<String> scopes)
{ {
throw new AccessTokenUnsupportedException(UNSUPPORTED_MESSAGE); throw new AccessTokenUnsupportedException(UNSUPPORTED_MESSAGE);
} }

View File

@ -29,3 +29,5 @@ keycloak.adapter.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
keycloak.synchronization.requiredClientScopes=realm-management

View File

@ -10,18 +10,502 @@
"en" "en"
], ],
"defaultLocale": "en", "defaultLocale": "en",
"clientScopes": [
{
"name": "profile",
"description": "OpenID Connect built-in scope: profile",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "${profileScopeConsentText}"
},
"protocolMappers": [
{
"name": "gender",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "gender",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "gender",
"jsonType.label": "String"
}
},
{
"name": "full name",
"protocol": "openid-connect",
"protocolMapper": "oidc-full-name-mapper",
"consentRequired": false,
"config": {
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
},
{
"name": "given name",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "firstName",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "given_name",
"jsonType.label": "String"
}
},
{
"name": "picture",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "picture",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "picture",
"jsonType.label": "String"
}
},
{
"name": "middle name",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "middleName",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "middle_name",
"jsonType.label": "String"
}
},
{
"name": "website",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "website",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "website",
"jsonType.label": "String"
}
},
{
"name": "birthdate",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "birthdate",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "birthdate",
"jsonType.label": "String"
}
},
{
"name": "family name",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "lastName",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "family_name",
"jsonType.label": "String"
}
},
{
"name": "locale",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "locale",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "locale",
"jsonType.label": "String"
}
},
{
"name": "zoneinfo",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "zoneinfo",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "zoneinfo",
"jsonType.label": "String"
}
},
{
"name": "profile",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "profile",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "profile",
"jsonType.label": "String"
}
},
{
"name": "nickname",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "nickname",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "nickname",
"jsonType.label": "String"
}
},
{
"name": "updated at",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "updatedAt",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "updated_at",
"jsonType.label": "String"
}
},
{
"name": "username",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "username",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "preferred_username",
"jsonType.label": "String"
}
}
]
},
{
"name": "email",
"description": "OpenID Connect built-in scope: email",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "${emailScopeConsentText}"
},
"protocolMappers": [
{
"name": "email",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "email",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "email",
"jsonType.label": "String"
}
},
{
"name": "email verified",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "emailVerified",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "email_verified",
"jsonType.label": "boolean"
}
}
]
},
{
"name": "address",
"description": "OpenID Connect built-in scope: address",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "${addressScopeConsentText}"
},
"protocolMappers": [
{
"name": "address",
"protocol": "openid-connect",
"protocolMapper": "oidc-address-mapper",
"consentRequired": false,
"config": {
"user.attribute.formatted": "formatted",
"user.attribute.country": "country",
"user.attribute.postal_code": "postal_code",
"userinfo.token.claim": "true",
"user.attribute.street": "street",
"id.token.claim": "true",
"user.attribute.region": "region",
"access.token.claim": "true",
"user.attribute.locality": "locality"
}
}
]
},
{
"name": "phone",
"description": "OpenID Connect built-in scope: phone",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "${phoneScopeConsentText}"
},
"protocolMappers": [
{
"name": "phone number",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "phoneNumber",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "phone_number",
"jsonType.label": "String"
}
},
{
"name": "phone number verified",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "phoneNumberVerified",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "phone_number_verified",
"jsonType.label": "boolean"
}
}
]
},
{
"name": "roles",
"description": "OpenID Connect scope for add user roles to the access token",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "true",
"consent.screen.text": "${rolesScopeConsentText}"
},
"protocolMappers": [
{
"name": "audience resolve",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-resolve-mapper",
"consentRequired": false
},
{
"name": "client roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-client-role-mapper",
"consentRequired": false,
"config": {
"user.attribute": "foo",
"access.token.claim": "true",
"claim.name": "resource_access.${client_id}.roles",
"jsonType.label": "String",
"multivalued": "true"
}
},
{
"name": "realm roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"user.attribute": "foo",
"access.token.claim": "true",
"claim.name": "realm_access.roles",
"jsonType.label": "String",
"multivalued": "true"
}
}
]
},
{
"name": "realm-roles",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "false"
},
"protocolMappers": [
{
"name": "realm roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"user.attribute": "foo",
"access.token.claim": "true",
"claim.name": "realm_access.roles",
"jsonType.label": "String",
"multivalued": "true"
}
}
]
},
{
"name": "realm-management",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "false"
},
"protocolMappers": [
{
"name": "Realm Management Client Roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-client-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "false",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "resource_access.realm-management.roles",
"jsonType.label": "String",
"usermodel.clientRoleMapping.clientId": "realm-management"
}
},
{
"name": "Realm Management Audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "realm-management",
"id.token.claim": "false",
"access.token.claim": "true"
}
}
]
},
{
"name": "alfresco",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "false"
},
"protocolMappers": [
{
"name": "Alfresco Client Roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-client-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "false",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "resource_access.alfresco.roles",
"jsonType.label": "String",
"usermodel.clientRoleMapping.clientId": "alfresco"
}
},
{
"name": "Alfresco Audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "alfresco",
"id.token.claim": "false",
"access.token.claim": "true"
}
}
]
},
{
"name": "web-origins",
"description": "OpenID Connect scope for add allowed web origins to the access token",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "false",
"consent.screen.text": ""
},
"protocolMappers": [
{
"name": "allowed web origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-allowed-origins-mapper",
"consentRequired": false
}
]
}
],
"defaultDefaultClientScopes": [
"profile",
"email",
"roles",
"web-origins"
],
"defaultOptionalClientScopes": [
"address",
"phone"
],
"clients": [ "clients": [
{ {
"id": "alfresco", "id": "alfresco",
"clientId": "alfresco", "clientId": "alfresco",
"name": "Alfresco Repository", "name": "Alfresco Repository",
"baseUrl": "http://localhost:${docker.tests.repositoryPort}/alfresco", "rootUrl": "http://localhost:${docker.tests.repositoryPort}/alfresco",
"adminUrl": "http://localhost:${docker.tests.repositoryPort}/alfresco/keycloak", "adminUrl": "http://localhost:${docker.tests.repositoryPort}/alfresco/keycloak",
"baseUrl": "/",
"redirectUris": [ "redirectUris": [
"http://localhost:${docker.tests.repositoryPort}/alfresco/*" "/*"
], ],
"webOrigins": [ "webOrigins": [
"http://localhost:${docker.tests.repositoryPort}/alfresco" "/"
], ],
"enabled": true, "enabled": true,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
@ -29,7 +513,18 @@
"directAccessGrantsEnabled": true, "directAccessGrantsEnabled": true,
"serviceAccountsEnabled": true, "serviceAccountsEnabled": true,
"publicClient": false, "publicClient": false,
"protocol": "openid-connect" "protocol": "openid-connect",
"defaultClientScopes": [
"profile",
"email",
"address",
"phone",
"realm-roles",
"alfresco"
],
"optionalClientScopes": [
"realm-management"
]
} }
], ],
"roles": { "roles": {

View File

@ -20,6 +20,8 @@ import static org.alfresco.web.site.SlingshotPageView.REDIRECT_URI;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -27,6 +29,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -177,6 +180,37 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
private static final ThreadLocal<String> LOGIN_REDIRECT_URL = new ThreadLocal<>(); private static final ThreadLocal<String> LOGIN_REDIRECT_URL = new ThreadLocal<>();
private static final BiFunction<HttpServletRequest, HttpServletResponse, ServletRequestAttributes> SERVLET_REQUEST_ATTRIBUTES_FACTORY;
static
{
BiFunction<HttpServletRequest, HttpServletResponse, ServletRequestAttributes> factory;
try
{
// try and use the overloaded constructor available in newer versions
final Constructor<ServletRequestAttributes> ctor = ServletRequestAttributes.class.getConstructor(HttpServletRequest.class,
HttpServletResponse.class);
factory = (req, res) -> {
try
{
return ctor.newInstance(req, res);
}
catch (final InstantiationException | IllegalAccessException | InvocationTargetException e)
{
throw new AlfrescoRuntimeException("Failed to construct servlet request attributes", e);
}
};
}
catch (final NoSuchMethodException nsme)
{
// fallback to constructor that's available in all Share versions
factory = (req, res) -> new ServletRequestAttributes(req);
}
SERVLET_REQUEST_ATTRIBUTES_FACTORY = factory;
}
protected ApplicationContext applicationContext; protected ApplicationContext applicationContext;
protected DependencyInjectedFilter defaultSsoFilter; protected DependencyInjectedFilter defaultSsoFilter;
@ -265,28 +299,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
final RemoteConfigElement remoteConfig = (RemoteConfigElement) this.configService.getConfig("Remote").getConfigElement("remote"); final RemoteConfigElement remoteConfig = (RemoteConfigElement) this.configService.getConfig("Remote").getConfigElement("remote");
if (remoteConfig != null) if (remoteConfig != null)
{ {
final EndpointDescriptor endpoint = remoteConfig.getEndpointDescriptor(this.primaryEndpoint); this.initFromRemoteEndpointConfig(remoteConfig);
if (endpoint != null)
{
this.externalAuthEnabled = endpoint.getExternalAuth();
}
else
{
LOGGER.error("Endpoint {} has not been defined in the application configuration", this.primaryEndpoint);
}
if (this.secondaryEndpoints != null)
{
this.secondaryEndpoints = this.secondaryEndpoints.stream().filter(secondaryEndpoint -> {
final boolean endpointExists = remoteConfig.getEndpointDescriptor(secondaryEndpoint) != null;
if (!endpointExists)
{
LOGGER.info("Excluding configured secondary endpoint {} which is not defined in the application configuration",
secondaryEndpoint);
}
return endpointExists;
}).collect(Collectors.toList());
}
} }
else else
{ {
@ -297,37 +310,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
.getConfig(KeycloakConfigConstants.KEYCLOAK_CONFIG_SECTION_NAME).getConfigElement(KeycloakAdapterConfigElement.NAME); .getConfig(KeycloakConfigConstants.KEYCLOAK_CONFIG_SECTION_NAME).getConfigElement(KeycloakAdapterConfigElement.NAME);
if (keycloakAdapterConfig != null) if (keycloakAdapterConfig != null)
{ {
final AdapterConfig adapterConfiguration = keycloakAdapterConfig.buildAdapterConfiguration(); this.initFromAdapterConfig(keycloakAdapterConfig);
// disable any CORS handling (if CORS is relevant, it should be handled by Share / Surf)
adapterConfiguration.setCors(false);
// BASIC authentication should never be used
adapterConfiguration.setEnableBasicAuth(false);
this.keycloakDeployment = KeycloakDeploymentBuilder.build(adapterConfiguration);
// even in newer version than used by ACS 6.x does Keycloak lib not allow timeout configuration
if (this.keycloakDeployment.getClient() != null)
{
final Long connectionTimeout = keycloakAdapterConfig.getConnectionTimeout();
final Long socketTimeout = keycloakAdapterConfig.getSocketTimeout();
HttpClientBuilder httpClientBuilder = new HttpClientBuilder();
if (connectionTimeout != null && connectionTimeout.longValue() >= 0)
{
httpClientBuilder = httpClientBuilder.establishConnectionTimeout(connectionTimeout.longValue(), TimeUnit.MILLISECONDS);
}
if (socketTimeout != null && socketTimeout.longValue() >= 0)
{
httpClientBuilder = httpClientBuilder.socketTimeout(socketTimeout.longValue(), TimeUnit.MILLISECONDS);
}
final HttpClient client = httpClientBuilder.build(adapterConfiguration);
this.configureForcedRouteIfNecessary(keycloakAdapterConfig, client);
this.keycloakDeployment.setClient(client);
}
this.deploymentContext = new AdapterDeploymentContext(this.keycloakDeployment);
} }
else else
{ {
@ -356,7 +339,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
/** /**
* @param defaultSsoFilter * @param defaultSsoFilter
* the defaultSsoFilter to set * the defaultSsoFilter to set
*/ */
public void setDefaultSsoFilter(final DependencyInjectedFilter defaultSsoFilter) public void setDefaultSsoFilter(final DependencyInjectedFilter defaultSsoFilter)
{ {
@ -365,7 +348,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
/** /**
* @param configService * @param configService
* the configService to set * the configService to set
*/ */
public void setConfigService(final ConfigService configService) public void setConfigService(final ConfigService configService)
{ {
@ -374,7 +357,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
/** /**
* @param connectorService * @param connectorService
* the connectorService to set * the connectorService to set
*/ */
public void setConnectorService(final ConnectorService connectorService) public void setConnectorService(final ConnectorService connectorService)
{ {
@ -383,7 +366,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
/** /**
* @param pageViewResolver * @param pageViewResolver
* the pageViewResolver to set * the pageViewResolver to set
*/ */
public void setPageViewResolver(final PageViewResolver pageViewResolver) public void setPageViewResolver(final PageViewResolver pageViewResolver)
{ {
@ -392,7 +375,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
/** /**
* @param sessionIdMapper * @param sessionIdMapper
* the sessionIdMapper to set * the sessionIdMapper to set
*/ */
public void setSessionIdMapper(final SessionIdMapper sessionIdMapper) public void setSessionIdMapper(final SessionIdMapper sessionIdMapper)
{ {
@ -401,7 +384,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
/** /**
* @param primaryEndpoint * @param primaryEndpoint
* the primaryEndpoint to set * the primaryEndpoint to set
*/ */
public void setPrimaryEndpoint(final String primaryEndpoint) public void setPrimaryEndpoint(final String primaryEndpoint)
{ {
@ -410,7 +393,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
/** /**
* @param secondaryEndpoints * @param secondaryEndpoints
* the secondaryEndpoints to set * the secondaryEndpoints to set
*/ */
public void setSecondaryEndpoints(final List<String> secondaryEndpoints) public void setSecondaryEndpoints(final List<String> secondaryEndpoints)
{ {
@ -439,8 +422,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
this.keycloakDeployment.getAuthServerBaseUrl()); this.keycloakDeployment.getAuthServerBaseUrl());
} }
// TODO Figure out how to support Enteprise 6.2 / 7.x or 6.3+, which overload the constructor RequestContextHolder.setRequestAttributes(SERVLET_REQUEST_ATTRIBUTES_FACTORY.apply(req, res));
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(req));
// Alfresco handling of RequestContext / ServletUtil / any other context holder is so immensely broken, it isn't even funny // Alfresco handling of RequestContext / ServletUtil / any other context holder is so immensely broken, it isn't even funny
// this request context is for any handling that needs it until it gets nuked / bulldozed by RequestContextInterceptor // this request context is for any handling that needs it until it gets nuked / bulldozed by RequestContextInterceptor
// ...after which we will have to enhance that class' partially initialised context // ...after which we will have to enhance that class' partially initialised context
@ -501,6 +483,67 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
} }
} }
protected void initFromRemoteEndpointConfig(final RemoteConfigElement remoteConfig)
{
final EndpointDescriptor endpoint = remoteConfig.getEndpointDescriptor(this.primaryEndpoint);
if (endpoint != null)
{
this.externalAuthEnabled = endpoint.getExternalAuth();
}
else
{
LOGGER.error("Endpoint {} has not been defined in the application configuration", this.primaryEndpoint);
}
if (this.secondaryEndpoints != null)
{
this.secondaryEndpoints = this.secondaryEndpoints.stream().filter(secondaryEndpoint -> {
final boolean endpointExists = remoteConfig.getEndpointDescriptor(secondaryEndpoint) != null;
if (!endpointExists)
{
LOGGER.info("Excluding configured secondary endpoint {} which is not defined in the application configuration",
secondaryEndpoint);
}
return endpointExists;
}).collect(Collectors.toList());
}
}
protected void initFromAdapterConfig(final KeycloakAdapterConfigElement keycloakAdapterConfig)
{
final AdapterConfig adapterConfiguration = keycloakAdapterConfig.buildAdapterConfiguration();
// disable any CORS handling (if CORS is relevant, it should be handled by Share / Surf)
adapterConfiguration.setCors(false);
// BASIC authentication should never be used
adapterConfiguration.setEnableBasicAuth(false);
this.keycloakDeployment = KeycloakDeploymentBuilder.build(adapterConfiguration);
// even in newer version than used by ACS 6.x does Keycloak lib not allow timeout configuration
if (this.keycloakDeployment.getClient() != null)
{
final Long connectionTimeout = keycloakAdapterConfig.getConnectionTimeout();
final Long socketTimeout = keycloakAdapterConfig.getSocketTimeout();
HttpClientBuilder httpClientBuilder = new HttpClientBuilder();
if (connectionTimeout != null && connectionTimeout.longValue() >= 0)
{
httpClientBuilder = httpClientBuilder.establishConnectionTimeout(connectionTimeout.longValue(), TimeUnit.MILLISECONDS);
}
if (socketTimeout != null && socketTimeout.longValue() >= 0)
{
httpClientBuilder = httpClientBuilder.socketTimeout(socketTimeout.longValue(), TimeUnit.MILLISECONDS);
}
final HttpClient client = httpClientBuilder.build(adapterConfiguration);
this.configureForcedRouteIfNecessary(keycloakAdapterConfig, client);
this.keycloakDeployment.setClient(client);
}
this.deploymentContext = new AdapterDeploymentContext(this.keycloakDeployment);
}
protected void processLogout(final ServletContext context, final HttpServletRequest req, final HttpServletResponse res, protected void processLogout(final ServletContext context, final HttpServletRequest req, final HttpServletResponse res,
final FilterChain chain) throws IOException, ServletException final FilterChain chain) throws IOException, ServletException
{ {
@ -544,17 +587,17 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* will be terminated. Otherwise processing may continue with the filter chain (if still applicable). * will be terminated. Otherwise processing may continue with the filter chain (if still applicable).
* *
* @param context * @param context
* the servlet context * the servlet context
* @param req * @param req
* the servlet request * the servlet request
* @param res * @param res
* the servlet response * the servlet response
* @param chain * @param chain
* the filter chain * the filter chain
* @throws IOException * @throws IOException
* if any error occurs during Keycloak authentication or processing of the filter chain * if any error occurs during Keycloak authentication or processing of the filter chain
* @throws ServletException * @throws ServletException
* if any error occurs during Keycloak authentication or processing of the filter chain * if any error occurs during Keycloak authentication or processing of the filter chain
*/ */
protected void processKeycloakAuthenticationAndActions(final ServletContext context, final HttpServletRequest req, protected void processKeycloakAuthenticationAndActions(final ServletContext context, final HttpServletRequest req,
final HttpServletResponse res, final FilterChain chain) throws IOException, ServletException final HttpServletResponse res, final FilterChain chain) throws IOException, ServletException
@ -622,21 +665,21 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Keycloak library access restrictions, in order to obtain the access token for validation and passing on to the Repository-tier. * Keycloak library access restrictions, in order to obtain the access token for validation and passing on to the Repository-tier.
* *
* @param context * @param context
* the servlet context * the servlet context
* @param req * @param req
* the servlet request * the servlet request
* @param res * @param res
* the servlet response * the servlet response
* @param chain * @param chain
* the filter chain * the filter chain
* @param performTokenExchange * @param performTokenExchange
* whether Share has been configured to perform OAuth2 token exchange to authenticate against the Repository backend * whether Share has been configured to perform OAuth2 token exchange to authenticate against the Repository backend
* @param facade * @param facade
* the Keycloak HTTP facade * the Keycloak HTTP facade
* @throws IOException * @throws IOException
* if any error occurs during Keycloak authentication or processing of the filter chain * if any error occurs during Keycloak authentication or processing of the filter chain
* @throws ServletException * @throws ServletException
* if any error occurs during Keycloak authentication or processing of the filter chain * 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, protected void processBearerAuthentication(final ServletContext context, final HttpServletRequest req, final HttpServletResponse res,
final FilterChain chain, final Boolean performTokenExchange, final OIDCServletHttpFacade facade) final FilterChain chain, final Boolean performTokenExchange, final OIDCServletHttpFacade facade)
@ -687,22 +730,22 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Processes the regular OIDC filter authentication on a request. * Processes the regular OIDC filter authentication on a request.
* *
* @param context * @param context
* the servlet context * the servlet context
* @param req * @param req
* the servlet request * the servlet request
* @param res * @param res
* the servlet response * the servlet response
* @param chain * @param chain
* the filter chain * the filter chain
* @param bodyBufferLimit * @param bodyBufferLimit
* the configured size limit to apply to any HTTP POST/PUT body buffering that may need to be applied to process the * 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 * authentication via an intermediary redirect
* @param facade * @param facade
* the Keycloak HTTP facade * the Keycloak HTTP facade
* @throws IOException * @throws IOException
* if any error occurs during Keycloak authentication or processing of the filter chain * if any error occurs during Keycloak authentication or processing of the filter chain
* @throws ServletException * @throws ServletException
* if any error occurs during Keycloak authentication or processing of the filter chain * 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, protected void processFilterAuthentication(final ServletContext context, final HttpServletRequest req, final HttpServletResponse res,
final FilterChain chain, final Integer bodyBufferLimit, final OIDCServletHttpFacade facade) throws IOException, ServletException final FilterChain chain, final Integer bodyBufferLimit, final OIDCServletHttpFacade facade) throws IOException, ServletException
@ -771,13 +814,13 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Sets up the necessary state to enhance the login form customisation to provide an action to perform a Keycloak login via a redirect. * Sets up the necessary state to enhance the login form customisation to provide an action to perform a Keycloak login via a redirect.
* *
* @param context * @param context
* the servlet context * the servlet context
* @param req * @param req
* the HTTP servlet request being processed * the HTTP servlet request being processed
* @param res * @param res
* the HTTP servlet response being processed * the HTTP servlet response being processed
* @param authenticator * @param authenticator
* the authenticator holding the challenge for a login redirect * the authenticator holding the challenge for a login redirect
*/ */
protected void prepareLoginFormEnhancement(final ServletContext context, final HttpServletRequest req, final HttpServletResponse res, protected void prepareLoginFormEnhancement(final ServletContext context, final HttpServletRequest req, final HttpServletResponse res,
final FilterRequestAuthenticator authenticator) final FilterRequestAuthenticator authenticator)
@ -807,11 +850,11 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Sets up the necessary state to enhance the login form customisation to provide an action to perform a Keycloak login via a redirect. * Sets up the necessary state to enhance the login form customisation to provide an action to perform a Keycloak login via a redirect.
* *
* @param context * @param context
* the servlet context * the servlet context
* @param req * @param req
* the HTTP servlet request being processed * the HTTP servlet request being processed
* @param res * @param res
* the HTTP servlet response being processed * the HTTP servlet response being processed
*/ */
protected void prepareLoginFormEnhancement(final ServletContext context, final HttpServletRequest req, final HttpServletResponse res) protected void prepareLoginFormEnhancement(final ServletContext context, final HttpServletRequest req, final HttpServletResponse res)
{ {
@ -877,21 +920,21 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Processes a successful authentication via Keycloak. * Processes a successful authentication via Keycloak.
* *
* @param context * @param context
* the servlet context * the servlet context
* @param req * @param req
* the servlet request * the servlet request
* @param res * @param res
* the servlet response * the servlet response
* @param chain * @param chain
* the filter chain * the filter chain
* @param facade * @param facade
* the Keycloak HTTP facade * the Keycloak HTTP facade
* @param tokenStore * @param tokenStore
* the Keycloak token store * the Keycloak token store
* @throws IOException * @throws IOException
* if any error occurs during Keycloak authentication or processing of the filter chain * if any error occurs during Keycloak authentication or processing of the filter chain
* @throws ServletException * @throws ServletException
* if any error occurs during Keycloak authentication or processing of the filter chain * if any error occurs during Keycloak authentication or processing of the filter chain
*/ */
protected void onKeycloakAuthenticationSuccess(final ServletContext context, final HttpServletRequest req, protected void onKeycloakAuthenticationSuccess(final ServletContext context, final HttpServletRequest req,
final HttpServletResponse res, final FilterChain chain, final OIDCServletHttpFacade facade, final HttpServletResponse res, final FilterChain chain, final OIDCServletHttpFacade facade,
@ -943,21 +986,21 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Processes a successful authentication via Keycloak. * Processes a successful authentication via Keycloak.
* *
* @param context * @param context
* the servlet context * the servlet context
* @param req * @param req
* the servlet request * the servlet request
* @param res * @param res
* the servlet response * the servlet response
* @param chain * @param chain
* the filter chain * the filter chain
* @param facade * @param facade
* the Keycloak HTTP facade * the Keycloak HTTP facade
* @param tokenHolder * @param tokenHolder
* the holder for access token taken from the successful authentication * the holder for access token taken from the successful authentication
* @throws IOException * @throws IOException
* if any error occurs during Keycloak authentication or processing of the filter chain * if any error occurs during Keycloak authentication or processing of the filter chain
* @throws ServletException * @throws ServletException
* if any error occurs during Keycloak authentication or processing of the filter chain * if any error occurs during Keycloak authentication or processing of the filter chain
*/ */
protected void onKeycloakAuthenticationSuccess(final ServletContext context, final HttpServletRequest req, protected void onKeycloakAuthenticationSuccess(final ServletContext context, final HttpServletRequest req,
final HttpServletResponse res, final FilterChain chain, final OIDCServletHttpFacade facade, final HttpServletResponse res, final FilterChain chain, final OIDCServletHttpFacade facade,
@ -1033,17 +1076,17 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Processes a failed authentication via Keycloak. * Processes a failed authentication via Keycloak.
* *
* @param context * @param context
* the servlet context * the servlet context
* @param req * @param req
* the servlet request * the servlet request
* @param res * @param res
* the servlet response * the servlet response
* @param chain * @param chain
* the filter chain * the filter chain
* @throws IOException * @throws IOException
* if any error occurs during processing of the filter chain * if any error occurs during processing of the filter chain
* @throws ServletException * @throws ServletException
* if any error occurs during processing of the filter chain * if any error occurs during processing of the filter chain
*/ */
protected void onKeycloakAuthenticationFailure(final ServletContext context, final HttpServletRequest req, protected void onKeycloakAuthenticationFailure(final ServletContext context, final HttpServletRequest req,
final HttpServletResponse res, final FilterChain chain) throws IOException, ServletException final HttpServletResponse res, final FilterChain chain) throws IOException, ServletException
@ -1137,9 +1180,9 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Completes the request context in the current thread by populating missing data, foremost any user details for the authenticated user. * Completes the request context in the current thread by populating missing data, foremost any user details for the authenticated user.
* *
* @param req * @param req
* the servlet request * the servlet request
* @throws ServletException * @throws ServletException
* if an error occurs populating the request context * if an error occurs populating the request context
*/ */
protected void completeRequestContext(final HttpServletRequest req) throws ServletException protected void completeRequestContext(final HttpServletRequest req) throws ServletException
{ {
@ -1158,17 +1201,17 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Continues processing the filter chain, either directly or by delegating to the facaded default SSO filter. * Continues processing the filter chain, either directly or by delegating to the facaded default SSO filter.
* *
* @param context * @param context
* the servlet context * the servlet context
* @param request * @param request
* the current request * the current request
* @param response * @param response
* the response to the current request * the response to the current request
* @param chain * @param chain
* the filter chain * the filter chain
* @throws IOException * @throws IOException
* if any exception is propagated by a filter in the chain or the actual request processing * if any exception is propagated by a filter in the chain or the actual request processing
* @throws ServletException * @throws ServletException
* if any exception is propagated by a filter in the chain or the actual request processing * if any exception is propagated by a filter in the chain or the actual request processing
*/ */
protected void continueFilterChain(final ServletContext context, final ServletRequest request, final ServletResponse response, protected void continueFilterChain(final ServletContext context, final ServletRequest request, final ServletResponse response,
final FilterChain chain) throws IOException, ServletException final FilterChain chain) throws IOException, ServletException
@ -1191,13 +1234,13 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Checks if processing of the filter must be skipped for the specified request. * Checks if processing of the filter must be skipped for the specified request.
* *
* @param req * @param req
* the servlet request to check for potential conditions to skip * the servlet request to check for potential conditions to skip
* @param res * @param res
* the servlet response on which potential updates of cookies / response headers need to be set * the servlet response on which potential updates of cookies / response headers need to be set
* @return {@code true} if processing of the {@link #doFilter(ServletContext, ServletRequest, ServletResponse, FilterChain) filter * @return {@code true} if processing of the {@link #doFilter(ServletContext, ServletRequest, ServletResponse, FilterChain) filter
* operation} must be skipped, {@code false} otherwise * operation} must be skipped, {@code false} otherwise
* @throws ServletException * @throws ServletException
* if any error occurs during inspection of the request * if any error occurs during inspection of the request
*/ */
protected boolean checkForSkipCondition(final HttpServletRequest req, final HttpServletResponse res) throws ServletException protected boolean checkForSkipCondition(final HttpServletRequest req, final HttpServletResponse res) throws ServletException
{ {
@ -1337,15 +1380,15 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* necessary or configured. * necessary or configured.
* *
* @param req * @param req
* the HTTP servlet request * the HTTP servlet request
* @param res * @param res
* the HTTP servlet response * the HTTP servlet response
* @param userId * @param userId
* the ID of the authenticated user * the ID of the authenticated user
* @param keycloakAccount * @param keycloakAccount
* the Keycloak account object * the Keycloak account object
* @return {@code true} if processing of the {@link #doFilter(ServletContext, ServletRequest, ServletResponse, FilterChain) filter * @return {@code true} if processing of the {@link #doFilter(ServletContext, ServletRequest, ServletResponse, FilterChain) filter
* operation} can be skipped as the account represents a valid and still active authentication, {@code false} otherwise * operation} can be skipped as the account represents a valid and still active authentication, {@code false} otherwise
*/ */
protected boolean validateAndRefreshKeycloakAuthentication(final HttpServletRequest req, final HttpServletResponse res, protected boolean validateAndRefreshKeycloakAuthentication(final HttpServletRequest req, final HttpServletResponse res,
final String userId, final KeycloakAccount keycloakAccount) final String userId, final KeycloakAccount keycloakAccount)
@ -1382,11 +1425,11 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Checks if the requested page does not require user authentication. * Checks if the requested page does not require user authentication.
* *
* @param req * @param req
* the servlet request for which to check the authentication requirement of the target page * the servlet request for which to check the authentication requirement of the target page
* @return {@code true} if the requested page does not require user authentication, * @return {@code true} if the requested page does not require user authentication,
* {@code false} otherwise (incl. failure to resolve the request to a target page) * {@code false} otherwise (incl. failure to resolve the request to a target page)
* @throws ServletException * @throws ServletException
* if any error occurs during inspection of the request * if any error occurs during inspection of the request
*/ */
protected boolean isNoAuthPage(final HttpServletRequest req) throws ServletException protected boolean isNoAuthPage(final HttpServletRequest req) throws ServletException
{ {
@ -1422,11 +1465,11 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Checks if the requested page is a login page. * Checks if the requested page is a login page.
* *
* @param req * @param req
* the request for which to check the type of page * the request for which to check the type of page
* @return {@code true} if the requested page is a login page, * @return {@code true} if the requested page is a login page,
* {@code false} otherwise (incl. failure to resolve the request to a target page) * {@code false} otherwise (incl. failure to resolve the request to a target page)
* @throws ServletException * @throws ServletException
* if any error occurs during inspection of the request * if any error occurs during inspection of the request
*/ */
protected boolean isLoginPage(final HttpServletRequest req) throws ServletException protected boolean isLoginPage(final HttpServletRequest req) throws ServletException
{ {
@ -1472,11 +1515,11 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Checks if the requested URL indicates a logout request. * Checks if the requested URL indicates a logout request.
* *
* @param req * @param req
* the request to check * the request to check
* @return {@code true} if the request is a request for logout, * @return {@code true} if the request is a request for logout,
* {@code false} otherwise * {@code false} otherwise
* @throws ServletException * @throws ServletException
* if any error occurs during inspection of the request * if any error occurs during inspection of the request
*/ */
protected boolean isLogoutRequest(final HttpServletRequest req) throws ServletException protected boolean isLogoutRequest(final HttpServletRequest req) throws ServletException
{ {
@ -1491,7 +1534,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Checks if the HTTP request has set the Keycloak state cookie. * Checks if the HTTP request has set the Keycloak state cookie.
* *
* @param req * @param req
* the HTTP request to check * the HTTP request to check
* @return {@code true} if the state cookie is set, {@code false} otherwise * @return {@code true} if the state cookie is set, {@code false} otherwise
*/ */
protected boolean hasStateCookie(final HttpServletRequest req) protected boolean hasStateCookie(final HttpServletRequest req)
@ -1509,9 +1552,9 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* authenticated user. * authenticated user.
* *
* @param req * @param req
* the request to check * the request to check
* @param session * @param session
* the active session managing any persistent access token state * the active session managing any persistent access token state
* @return {@code true} if the backend requires HTTP Basic or Keycloak authentication, {@code false} otherwise * @return {@code true} if the backend requires HTTP Basic or Keycloak authentication, {@code false} otherwise
*/ */
protected boolean isBackendRequiringBasicOrKeycloakAuthentication(final HttpServletRequest req, final HttpSession session) protected boolean isBackendRequiringBasicOrKeycloakAuthentication(final HttpServletRequest req, final HttpSession session)
@ -1588,7 +1631,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* state / validity of any existing token. * state / validity of any existing token.
* *
* @param session * @param session
* the active session managing any persistent access token state * the active session managing any persistent access token state
*/ */
protected void handleAlfrescoResourceAccessToken(final HttpSession session) protected void handleAlfrescoResourceAccessToken(final HttpSession session)
{ {
@ -1685,12 +1728,12 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* that backend resource. * that backend resource.
* *
* @param alfrescoResourceName * @param alfrescoResourceName
* the name of the Alfresco backend resource within the Keycloak realm * the name of the Alfresco backend resource within the Keycloak realm
* @param session * @param session
* the active session managing any persistent access token state * the active session managing any persistent access token state
* @return the response to obtaining the access token for the Alfresco backend * @return the response to obtaining the access token for the Alfresco backend
* @throws IOException * @throws IOException
* if any error occurs calling Keycloak to exchange the access token * if any error occurs calling Keycloak to exchange the access token
*/ */
protected AccessTokenResponse getAccessToken(final String alfrescoResourceName, final HttpSession session) throws IOException protected AccessTokenResponse getAccessToken(final String alfrescoResourceName, final HttpSession session) throws IOException
{ {
@ -1766,11 +1809,11 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Resets any Keycloak-related state cookies present in the current request. * Resets any Keycloak-related state cookies present in the current request.
* *
* @param context * @param context
* the servlet context * the servlet context
* @param req * @param req
* the servlet request * the servlet request
* @param res * @param res
* the servlet response * the servlet response
*/ */
protected void resetStateCookies(final ServletContext context, final HttpServletRequest req, final HttpServletResponse res) protected void resetStateCookies(final ServletContext context, final HttpServletRequest req, final HttpServletResponse res)
{ {
@ -1794,7 +1837,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* technical default value in lieu of an explicitly configured value. * technical default value in lieu of an explicitly configured value.
* *
* @param req * @param req
* the incoming request * the incoming request
* @return the assumed SSL port to be used in redirects * @return the assumed SSL port to be used in redirects
*/ */
protected int determineLikelySslPort(final HttpServletRequest req) protected int determineLikelySslPort(final HttpServletRequest req)
@ -1827,9 +1870,9 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* directly, due to network isolation / addressing restrictions (e.g. in Docker-ized deployments). * directly, due to network isolation / addressing restrictions (e.g. in Docker-ized deployments).
* *
* @param configElement * @param configElement
* the adapter configuration * the adapter configuration
* @param client * @param client
* the client to configure * 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)

View File

@ -18,7 +18,7 @@
authentication.chain=keycloak1:keycloak,alfrescoNtlm1:alfrescoNtlm authentication.chain=keycloak1:keycloak,alfrescoNtlm1:alfrescoNtlm
keycloak.adapter.auth-server-url=http://${docker.tests.host.name}:${docker.tests.keycloakPort}/auth keycloak.adapter.auth-server-url=http://localhost:${docker.tests.keycloakPort}/auth
keycloak.adapter.realm=test keycloak.adapter.realm=test
keycloak.adapter.resource=alfresco keycloak.adapter.resource=alfresco
keycloak.adapter.credentials.provider=secret keycloak.adapter.credentials.provider=secret
@ -29,3 +29,5 @@ keycloak.adapter.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
keycloak.synchronization.requiredClientScopes=realm-management

View File

@ -10,18 +10,552 @@
"en" "en"
], ],
"defaultLocale": "en", "defaultLocale": "en",
"clientScopes": [
{
"name": "profile",
"description": "OpenID Connect built-in scope: profile",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "${profileScopeConsentText}"
},
"protocolMappers": [
{
"name": "gender",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "gender",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "gender",
"jsonType.label": "String"
}
},
{
"name": "full name",
"protocol": "openid-connect",
"protocolMapper": "oidc-full-name-mapper",
"consentRequired": false,
"config": {
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
},
{
"name": "given name",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "firstName",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "given_name",
"jsonType.label": "String"
}
},
{
"name": "picture",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "picture",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "picture",
"jsonType.label": "String"
}
},
{
"name": "middle name",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "middleName",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "middle_name",
"jsonType.label": "String"
}
},
{
"name": "website",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "website",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "website",
"jsonType.label": "String"
}
},
{
"name": "birthdate",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "birthdate",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "birthdate",
"jsonType.label": "String"
}
},
{
"name": "family name",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "lastName",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "family_name",
"jsonType.label": "String"
}
},
{
"name": "locale",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "locale",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "locale",
"jsonType.label": "String"
}
},
{
"name": "zoneinfo",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "zoneinfo",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "zoneinfo",
"jsonType.label": "String"
}
},
{
"name": "profile",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "profile",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "profile",
"jsonType.label": "String"
}
},
{
"name": "nickname",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "nickname",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "nickname",
"jsonType.label": "String"
}
},
{
"name": "updated at",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "updatedAt",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "updated_at",
"jsonType.label": "String"
}
},
{
"name": "username",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "username",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "preferred_username",
"jsonType.label": "String"
}
}
]
},
{
"name": "email",
"description": "OpenID Connect built-in scope: email",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "${emailScopeConsentText}"
},
"protocolMappers": [
{
"name": "email",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "email",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "email",
"jsonType.label": "String"
}
},
{
"name": "email verified",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "emailVerified",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "email_verified",
"jsonType.label": "boolean"
}
}
]
},
{
"name": "address",
"description": "OpenID Connect built-in scope: address",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "${addressScopeConsentText}"
},
"protocolMappers": [
{
"name": "address",
"protocol": "openid-connect",
"protocolMapper": "oidc-address-mapper",
"consentRequired": false,
"config": {
"user.attribute.formatted": "formatted",
"user.attribute.country": "country",
"user.attribute.postal_code": "postal_code",
"userinfo.token.claim": "true",
"user.attribute.street": "street",
"id.token.claim": "true",
"user.attribute.region": "region",
"access.token.claim": "true",
"user.attribute.locality": "locality"
}
}
]
},
{
"name": "phone",
"description": "OpenID Connect built-in scope: phone",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "${phoneScopeConsentText}"
},
"protocolMappers": [
{
"name": "phone number",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "phoneNumber",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "phone_number",
"jsonType.label": "String"
}
},
{
"name": "phone number verified",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "phoneNumberVerified",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "phone_number_verified",
"jsonType.label": "boolean"
}
}
]
},
{
"name": "roles",
"description": "OpenID Connect scope for add user roles to the access token",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "true",
"consent.screen.text": "${rolesScopeConsentText}"
},
"protocolMappers": [
{
"name": "audience resolve",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-resolve-mapper",
"consentRequired": false
},
{
"name": "client roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-client-role-mapper",
"consentRequired": false,
"config": {
"user.attribute": "foo",
"access.token.claim": "true",
"claim.name": "resource_access.${client_id}.roles",
"jsonType.label": "String",
"multivalued": "true"
}
},
{
"name": "realm roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"user.attribute": "foo",
"access.token.claim": "true",
"claim.name": "realm_access.roles",
"jsonType.label": "String",
"multivalued": "true"
}
}
]
},
{
"name": "realm-roles",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "false"
},
"protocolMappers": [
{
"name": "realm roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"user.attribute": "foo",
"access.token.claim": "true",
"claim.name": "realm_access.roles",
"jsonType.label": "String",
"multivalued": "true"
}
}
]
},
{
"name": "realm-management",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "false"
},
"protocolMappers": [
{
"name": "Realm Management Client Roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-client-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "false",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "resource_access.realm-management.roles",
"jsonType.label": "String",
"usermodel.clientRoleMapping.clientId": "realm-management"
}
},
{
"name": "Realm Management Audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "realm-management",
"id.token.claim": "false",
"access.token.claim": "true"
}
}
]
},
{
"name": "alfresco",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "false"
},
"protocolMappers": [
{
"name": "Alfresco Client Roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-client-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "false",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "resource_access.alfresco.roles",
"jsonType.label": "String",
"usermodel.clientRoleMapping.clientId": "alfresco"
}
},
{
"name": "Alfresco Audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "alfresco",
"id.token.claim": "false",
"access.token.claim": "true"
}
}
]
},
{
"name": "alfresco-share",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "false"
},
"protocolMappers": [
{
"name": "Alfresco Share Client Roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-client-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "false",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "resource_access.alfresco-share.roles",
"jsonType.label": "String",
"usermodel.clientRoleMapping.clientId": "alfresco-share"
}
},
{
"name": "Alfresco Share Audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "alfresco-share",
"id.token.claim": "false",
"access.token.claim": "true"
}
},
{
"name": "username",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "username",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "preferred_username",
"jsonType.label": "String"
}
}
]
},
{
"name": "web-origins",
"description": "OpenID Connect scope for add allowed web origins to the access token",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "false",
"consent.screen.text": ""
},
"protocolMappers": [
{
"name": "allowed web origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-allowed-origins-mapper",
"consentRequired": false
}
]
}
],
"defaultDefaultClientScopes": [
"profile",
"email",
"roles",
"web-origins"
],
"defaultOptionalClientScopes": [
"address",
"phone"
],
"clients": [ "clients": [
{ {
"id": "alfresco", "id": "alfresco",
"clientId": "alfresco", "clientId": "alfresco",
"name": "Alfresco Repository", "name": "Alfresco Repository",
"baseUrl": "http://localhost:${docker.tests.repositoryPort}/alfresco", "rootUrl": "http://localhost:${docker.tests.repositoryPort}/alfresco",
"adminUrl": "http://localhost:${docker.tests.repositoryPort}/alfresco/keycloak", "adminUrl": "http://localhost:${docker.tests.repositoryPort}/alfresco/keycloak",
"baseUrl": "/",
"redirectUris": [ "redirectUris": [
"http://localhost:${docker.tests.repositoryPort}/alfresco/*" "/*"
], ],
"webOrigins": [ "webOrigins": [
"http://localhost:${docker.tests.repositoryPort}/alfresco" "/"
], ],
"enabled": true, "enabled": true,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
@ -29,25 +563,41 @@
"directAccessGrantsEnabled": true, "directAccessGrantsEnabled": true,
"serviceAccountsEnabled": true, "serviceAccountsEnabled": true,
"publicClient": false, "publicClient": false,
"protocol": "openid-connect" "protocol": "openid-connect",
"defaultClientScopes": [
"profile",
"email",
"address",
"phone",
"realm-roles",
"alfresco"
],
"optionalClientScopes": [
"realm-management"
]
}, },
{ {
"id": "alfresco-share", "id": "alfresco-share",
"clientId": "alfresco-share", "clientId": "alfresco-share",
"name": "Alfresco Share", "name": "Alfresco Share",
"baseUrl": "http://localhost:${docker.tests.sharePort}/share", "rootUrl": "http://localhost:${docker.tests.sharePort}/share",
"adminUrl": "http://localhost:${docker.tests.sharePort}/share/keycloak", "adminUrl": "http://localhost:${docker.tests.sharePort}/share/keycloak",
"baseUrl": "/",
"redirectUris": [ "redirectUris": [
"http://localhost:${docker.tests.sharePort}/share/*" "/*"
], ],
"webOrigins": [ "webOrigins": [
"http://localhost:${docker.tests.sharePort}/share" "/"
], ],
"enabled": true, "enabled": true,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"secret": "a5b3e8bc-39cc-4ddd-8c8f-1c34e7a35975", "secret": "a5b3e8bc-39cc-4ddd-8c8f-1c34e7a35975",
"publicClient": false, "publicClient": false,
"protocol": "openid-connect" "protocol": "openid-connect",
"defaultClientScopes": [
"realm-roles",
"alfresco-share"
]
}, },
{ {
"clientId": "realm-management", "clientId": "realm-management",
@ -250,6 +800,8 @@
"manage-account" "manage-account"
], ],
"realm-management": [ "realm-management": [
"query-groups",
"query-users",
"view-users", "view-users",
"view-clients" "view-clients"
] ]