diff --git a/repository/src/main/globalConfig/subsystems/Authentication/keycloak/keycloak-authentication-context.xml b/repository/src/main/globalConfig/subsystems/Authentication/keycloak/keycloak-authentication-context.xml
index e39bebf..1c34313 100644
--- a/repository/src/main/globalConfig/subsystems/Authentication/keycloak/keycloak-authentication-context.xml
+++ b/repository/src/main/globalConfig/subsystems/Authentication/keycloak/keycloak-authentication-context.xml
@@ -162,6 +162,7 @@
+
diff --git a/repository/src/main/globalConfig/subsystems/Authentication/keycloak/keycloak-authentication.properties b/repository/src/main/globalConfig/subsystems/Authentication/keycloak/keycloak-authentication.properties
index 5450bc2..fdc8a62 100644
--- a/repository/src/main/globalConfig/subsystems/Authentication/keycloak/keycloak-authentication.properties
+++ b/repository/src/main/globalConfig/subsystems/Authentication/keycloak/keycloak-authentication.properties
@@ -97,6 +97,7 @@ keycloak.roles.resourceMapper.default.prefix.property.prefix=ROLE_KEYCLOAK_${key
keycloak.synchronization.enabled=true
keycloak.synchronization.user=
keycloak.synchronization.password=
+keycloak.synchronization.requiredClientScopes=
keycloak.synchronization.personLoadBatchSize=50
keycloak.synchronization.groupLoadBatchSize=50
diff --git a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationComponent.java b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationComponent.java
index 35c752b..041c310 100644
--- a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationComponent.java
+++ b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationComponent.java
@@ -120,7 +120,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
/**
* @param active
- * the active to set
+ * the active to set
*/
public void setActive(final boolean active)
{
@@ -138,7 +138,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
/**
* @param allowUserNamePasswordLogin
- * the allowUserNamePasswordLogin to set
+ * the allowUserNamePasswordLogin to set
*/
public void setAllowUserNamePasswordLogin(final boolean allowUserNamePasswordLogin)
{
@@ -147,7 +147,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
/**
* @param failExpiredTicketTokens
- * the failExpiredTicketTokens to set
+ * the failExpiredTicketTokens to set
*/
public void setFailExpiredTicketTokens(final boolean failExpiredTicketTokens)
{
@@ -156,7 +156,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
/**
* @param allowGuestLogin
- * the allowGuestLogin to set
+ * the allowGuestLogin to set
*/
public void setAllowGuestLogin(final boolean allowGuestLogin)
{
@@ -166,7 +166,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
/**
* @param allowGuestLogin
- * the allowGuestLogin to set
+ * the allowGuestLogin to set
*/
@Override
public void setAllowGuestLogin(final Boolean allowGuestLogin)
@@ -176,7 +176,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
/**
* @param mapAuthorities
- * the mapAuthorities to set
+ * the mapAuthorities to set
*/
public void setMapAuthorities(final boolean mapAuthorities)
{
@@ -185,7 +185,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
/**
* @param mapPersonPropertiesOnLogin
- * the mapPersonPropertiesOnLogin to set
+ * the mapPersonPropertiesOnLogin to set
*/
public void setMapPersonPropertiesOnLogin(final boolean mapPersonPropertiesOnLogin)
{
@@ -194,7 +194,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
/**
* @param deployment
- * the deployment to set
+ * the deployment to set
*/
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.
*
* @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 -
- * 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)
throws AuthenticationException
@@ -288,7 +288,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
String realUserName = userName;
try
{
- accessTokenHolder = this.accessTokenClient.obtainAccessToken(userName, new String(password));
+ accessTokenHolder = this.accessTokenClient.obtainAccessToken(userName, new String(password), Collections.emptySet());
realUserName = accessTokenHolder.getAccessToken().getPreferredUsername();
// 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.
*
* @param accessToken
- * the access token
+ * the access token
* @param idToken
- * the ID token
+ * the ID token
* @param freshLogin
- * {@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
- * handled if enabled
+ * {@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
+ * handled if enabled
*/
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.
*
* @param accessToken
- * the access token
+ * the access token
* @param idToken
- * the ID token
+ * the ID token
*/
protected void updatePerson(final AccessToken accessToken, final IDToken idToken)
{
diff --git a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/client/IDMClientImpl.java b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/client/IDMClientImpl.java
index 6080101..4cf3461 100644
--- a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/client/IDMClientImpl.java
+++ b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/client/IDMClientImpl.java
@@ -21,6 +21,9 @@ import com.fasterxml.jackson.databind.MappingIterator;
import java.io.IOException;
import java.io.InputStream;
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.function.Consumer;
@@ -65,6 +68,8 @@ public class IDMClientImpl implements InitializingBean, IDMClient
protected String password;
+ protected final Collection requiredClientScopes = new HashSet<>();
+
protected AccessTokenHolder accessToken;
/**
@@ -79,7 +84,7 @@ public class IDMClientImpl implements InitializingBean, IDMClient
/**
* @param deployment
- * the deployment to set
+ * the deployment to set
*/
public void setDeployment(final KeycloakDeployment deployment)
{
@@ -88,7 +93,7 @@ public class IDMClientImpl implements InitializingBean, IDMClient
/**
* @param accessTokenService
- * the accessTokenService to set
+ * the accessTokenService to set
*/
public void setAccessTokenService(final AccessTokenService accessTokenService)
{
@@ -97,7 +102,7 @@ public class IDMClientImpl implements InitializingBean, IDMClient
/**
* @param userName
- * the userName to set
+ * the userName to set
*/
public void setUserName(final String userName)
{
@@ -106,13 +111,26 @@ public class IDMClientImpl implements InitializingBean, IDMClient
/**
* @param password
- * the password to set
+ * the password to set
*/
public void setPassword(final String 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}
@@ -414,13 +432,13 @@ public class IDMClientImpl implements InitializingBean, IDMClient
* Loads and processes a batch of generic entities from Keycloak.
*
* @param
- * the type of the response entities
+ * the type of the response entities
* @param uri
- * the URI to call
+ * the URI to call
* @param entityProcessor
- * the processor handling the loaded entities
+ * the processor handling the loaded entities
* @param entityClass
- * the type of the expected response entities
+ * the type of the expected response entities
* @return the number of processed entities
*/
protected int processEntityBatch(final URI uri, final Consumer entityProcessor, final Class entityClass)
@@ -483,9 +501,9 @@ public class IDMClientImpl implements InitializingBean, IDMClient
* Executes a generic HTTP GET operation yielding a JSON response.
*
* @param uri
- * the URI to call
+ * the URI to call
* @param responseProcessor
- * the processor handling the response JSON
+ * the processor handling the response JSON
*/
protected void processGenericGet(final URI uri, final Consumer responseProcessor)
{
@@ -539,11 +557,11 @@ public class IDMClientImpl implements InitializingBean, IDMClient
* Executes a generic HTTP GET operation yielding a mapped response entity.
*
* @param
- * the type of the response entity
+ * the type of the response entity
* @param uri
- * the URI to call
+ * the URI to call
* @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
*
*/
@@ -610,11 +628,12 @@ public class IDMClientImpl implements InitializingBean, IDMClient
{
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
{
- this.accessToken = this.accessTokenService.obtainAccessToken();
+ this.accessToken = this.accessTokenService.obtainAccessToken(this.requiredClientScopes);
}
}
}
diff --git a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/AccessTokenClient.java b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/AccessTokenClient.java
index ba92edb..cc045db 100644
--- a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/AccessTokenClient.java
+++ b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/AccessTokenClient.java
@@ -3,6 +3,7 @@ package de.acosix.alfresco.keycloak.repo.token;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
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
* 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
* @throws AccessTokenException
- * if the access token cannot be obtained
+ * if the access token cannot be obtained
*/
- public RefreshableAccessTokenHolder obtainAccessToken()
+ public RefreshableAccessTokenHolder obtainAccessToken(final Collection scopes)
{
- LOGGER.debug("Obtaining client access token");
+ ParameterCheck.mandatory("scopes", scopes);
+ LOGGER.debug("Obtaining client access token with (optional) scopes {}", scopes);
try
{
final AccessTokenResponse response = this.getAccessTokenImpl(formParams -> {
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
+ this.processScopes(scopes, formParams);
});
final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response);
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.
*
* @param user
- * the name of the user
+ * the name of the user
* @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
* @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 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
{
final AccessTokenResponse response = this.getAccessTokenImpl(formParams -> {
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
formParams.add(new BasicNameValuePair("username", user));
formParams.add(new BasicNameValuePair("password", password));
+ this.processScopes(scopes, formParams);
});
final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response);
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.
*
* @param accessToken
- * the access token to exchange
+ * the access token to exchange
* @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
* @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 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
{
final AccessTokenResponse response = this.getAccessTokenImpl(formParams -> {
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE));
formParams.add(new BasicNameValuePair(OAuth2Constants.AUDIENCE, client));
formParams.add(new BasicNameValuePair(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.REFRESH_TOKEN_TYPE));
+ this.processScopes(scopes, formParams);
});
final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response, client);
final RefreshableAccessTokenHolder refreshableToken = new RefreshableAccessTokenHolder(response, verifiedTokens);
@@ -150,10 +169,10 @@ public class AccessTokenClient
* Refreshes an access token via a previously obtained refresh token.
*
* @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
* @throws AccessTokenRefreshException
- * if the refresh failed
+ * if the refresh failed
*/
public RefreshableAccessTokenHolder refreshAccessToken(final String refreshToken)
{
@@ -178,6 +197,23 @@ public class AccessTokenClient
}
}
+ protected void processScopes(final Collection scopes, final List 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)
{
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.
*
* @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
* @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
// authz-client does not support refresh, so would be of limited value anyway
diff --git a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/AccessTokenService.java b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/AccessTokenService.java
index 9ed87dd..67df9f8 100644
--- a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/AccessTokenService.java
+++ b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/AccessTokenService.java
@@ -15,6 +15,9 @@
*/
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.
*
@@ -28,35 +31,85 @@ public interface AccessTokenService
*
* @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
+ * 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 scopes);
/**
* Obtains a generic realm access token for a specific user of the realm.
*
* @param user
- * the name of the user
+ * the name of the user
* @param password
- * the password of the user
+ * the password of the user
* @return the holder for the access token
* @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
- * client of this Alfresco instance
+ * if no access token for the user can be obtained
*/
- 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 scopes);
/**
* 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
+ * the access token to exchange for token to another 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
* @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 scopes);
}
diff --git a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/AccessTokenServiceImpl.java b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/AccessTokenServiceImpl.java
index 2a1d1f0..8457515 100644
--- a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/AccessTokenServiceImpl.java
+++ b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/AccessTokenServiceImpl.java
@@ -15,6 +15,8 @@
*/
package de.acosix.alfresco.keycloak.repo.token;
+import java.util.Collection;
+
import org.alfresco.util.ParameterCheck;
import org.alfresco.util.PropertyCheck;
import org.keycloak.adapters.KeycloakDeployment;
@@ -47,7 +49,7 @@ public class AccessTokenServiceImpl implements AccessTokenService, InitializingB
/**
* @param deployment
- * the deployment to set
+ * the deployment to set
*/
public void setDeployment(final KeycloakDeployment deployment)
{
@@ -59,15 +61,16 @@ public class AccessTokenServiceImpl implements AccessTokenService, InitializingB
* {@inheritDoc}
*/
@Override
- public AccessTokenHolder obtainAccessToken()
+ public AccessTokenHolder obtainAccessToken(final Collection 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(),
this.accessTokenClient::refreshAccessToken, () -> {
try
{
- return this.accessTokenClient.obtainAccessToken();
+ return this.accessTokenClient.obtainAccessToken(scopes);
}
catch (final AccessTokenException atex)
{
@@ -81,18 +84,18 @@ public class AccessTokenServiceImpl implements AccessTokenService, InitializingB
* {@inheritDoc}
*/
@Override
- public AccessTokenHolder obtainAccessToken(final String user, final String password)
+ public AccessTokenHolder obtainAccessToken(final String user, final String password, final Collection scopes)
{
ParameterCheck.mandatoryString("user", user);
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(),
this.accessTokenClient::refreshAccessToken, () -> {
try
{
- return this.accessTokenClient.obtainAccessToken(user, password);
+ return this.accessTokenClient.obtainAccessToken(user, password, scopes);
}
catch (final AccessTokenException atex)
{
@@ -105,18 +108,18 @@ public class AccessTokenServiceImpl implements AccessTokenService, InitializingB
* {@inheritDoc}
*/
@Override
- public AccessTokenHolder exchangeToken(final String accessToken, final String client)
+ public AccessTokenHolder exchangeToken(final String accessToken, final String client, final Collection scopes)
{
ParameterCheck.mandatoryString("accessToken", accessToken);
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 -> {
try
{
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)
{
diff --git a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/NoOpAccessTokenServiceImpl.java b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/NoOpAccessTokenServiceImpl.java
index f05fce0..19af687 100644
--- a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/NoOpAccessTokenServiceImpl.java
+++ b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/NoOpAccessTokenServiceImpl.java
@@ -15,6 +15,8 @@
*/
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
* if no Keycloak subsystem instance is active.
@@ -31,7 +33,7 @@ public class NoOpAccessTokenServiceImpl implements AccessTokenService
* {@inheritDoc}
*/
@Override
- public AccessTokenHolder obtainAccessToken()
+ public AccessTokenHolder obtainAccessToken(final Collection scopes)
{
throw new AccessTokenUnsupportedException(UNSUPPORTED_MESSAGE);
}
@@ -41,7 +43,7 @@ public class NoOpAccessTokenServiceImpl implements AccessTokenService
* {@inheritDoc}
*/
@Override
- public AccessTokenHolder obtainAccessToken(final String user, final String password)
+ public AccessTokenHolder obtainAccessToken(final String user, final String password, final Collection scopes)
{
throw new AccessTokenUnsupportedException(UNSUPPORTED_MESSAGE);
}
@@ -51,7 +53,7 @@ public class NoOpAccessTokenServiceImpl implements AccessTokenService
* {@inheritDoc}
*/
@Override
- public AccessTokenHolder exchangeToken(final String accessToken, final String client)
+ public AccessTokenHolder exchangeToken(final String accessToken, final String client, final Collection scopes)
{
throw new AccessTokenUnsupportedException(UNSUPPORTED_MESSAGE);
}
diff --git a/repository/src/test/docker/alfresco/extension/alfresco-global.addition.properties b/repository/src/test/docker/alfresco/extension/alfresco-global.addition.properties
index 969d1f9..b68d867 100644
--- a/repository/src/test/docker/alfresco/extension/alfresco-global.addition.properties
+++ b/repository/src/test/docker/alfresco/extension/alfresco-global.addition.properties
@@ -29,3 +29,5 @@ keycloak.adapter.directAuthHost=http://keycloak:8080
keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=/Test A
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=/Test A
+
+keycloak.synchronization.requiredClientScopes=realm-management
\ No newline at end of file
diff --git a/repository/src/test/docker/test-realm.json b/repository/src/test/docker/test-realm.json
index 3e5eaa6..8c497e1 100644
--- a/repository/src/test/docker/test-realm.json
+++ b/repository/src/test/docker/test-realm.json
@@ -10,18 +10,502 @@
"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": [
{
"id": "alfresco",
"clientId": "alfresco",
"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",
+ "baseUrl": "/",
"redirectUris": [
- "http://localhost:${docker.tests.repositoryPort}/alfresco/*"
+ "/*"
],
"webOrigins": [
- "http://localhost:${docker.tests.repositoryPort}/alfresco"
+ "/"
],
"enabled": true,
"clientAuthenticatorType": "client-secret",
@@ -29,7 +513,18 @@
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": true,
"publicClient": false,
- "protocol": "openid-connect"
+ "protocol": "openid-connect",
+ "defaultClientScopes": [
+ "profile",
+ "email",
+ "address",
+ "phone",
+ "realm-roles",
+ "alfresco"
+ ],
+ "optionalClientScopes": [
+ "realm-management"
+ ]
}
],
"roles": {
diff --git a/share/src/main/java/de/acosix/alfresco/keycloak/share/web/KeycloakAuthenticationFilter.java b/share/src/main/java/de/acosix/alfresco/keycloak/share/web/KeycloakAuthenticationFilter.java
index cb8a0fa..8050d11 100644
--- a/share/src/main/java/de/acosix/alfresco/keycloak/share/web/KeycloakAuthenticationFilter.java
+++ b/share/src/main/java/de/acosix/alfresco/keycloak/share/web/KeycloakAuthenticationFilter.java
@@ -20,6 +20,8 @@ import static org.alfresco.web.site.SlingshotPageView.REDIRECT_URI;
import java.io.IOException;
import java.io.InputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
@@ -27,6 +29,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
+import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -177,6 +180,37 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
private static final ThreadLocal LOGIN_REDIRECT_URL = new ThreadLocal<>();
+ private static final BiFunction SERVLET_REQUEST_ATTRIBUTES_FACTORY;
+
+ static
+ {
+ BiFunction factory;
+
+ try
+ {
+ // try and use the overloaded constructor available in newer versions
+ final Constructor 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 DependencyInjectedFilter defaultSsoFilter;
@@ -265,28 +299,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
final RemoteConfigElement remoteConfig = (RemoteConfigElement) this.configService.getConfig("Remote").getConfigElement("remote");
if (remoteConfig != null)
{
- 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());
- }
+ this.initFromRemoteEndpointConfig(remoteConfig);
}
else
{
@@ -297,37 +310,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
.getConfig(KeycloakConfigConstants.KEYCLOAK_CONFIG_SECTION_NAME).getConfigElement(KeycloakAdapterConfigElement.NAME);
if (keycloakAdapterConfig != null)
{
- 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);
+ this.initFromAdapterConfig(keycloakAdapterConfig);
}
else
{
@@ -356,7 +339,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
/**
* @param defaultSsoFilter
- * the defaultSsoFilter to set
+ * the defaultSsoFilter to set
*/
public void setDefaultSsoFilter(final DependencyInjectedFilter defaultSsoFilter)
{
@@ -365,7 +348,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
/**
* @param configService
- * the configService to set
+ * the configService to set
*/
public void setConfigService(final ConfigService configService)
{
@@ -374,7 +357,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
/**
* @param connectorService
- * the connectorService to set
+ * the connectorService to set
*/
public void setConnectorService(final ConnectorService connectorService)
{
@@ -383,7 +366,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
/**
* @param pageViewResolver
- * the pageViewResolver to set
+ * the pageViewResolver to set
*/
public void setPageViewResolver(final PageViewResolver pageViewResolver)
{
@@ -392,7 +375,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
/**
* @param sessionIdMapper
- * the sessionIdMapper to set
+ * the sessionIdMapper to set
*/
public void setSessionIdMapper(final SessionIdMapper sessionIdMapper)
{
@@ -401,7 +384,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
/**
* @param primaryEndpoint
- * the primaryEndpoint to set
+ * the primaryEndpoint to set
*/
public void setPrimaryEndpoint(final String primaryEndpoint)
{
@@ -410,7 +393,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
/**
* @param secondaryEndpoints
- * the secondaryEndpoints to set
+ * the secondaryEndpoints to set
*/
public void setSecondaryEndpoints(final List secondaryEndpoints)
{
@@ -439,8 +422,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
this.keycloakDeployment.getAuthServerBaseUrl());
}
- // TODO Figure out how to support Enteprise 6.2 / 7.x or 6.3+, which overload the constructor
- RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(req));
+ RequestContextHolder.setRequestAttributes(SERVLET_REQUEST_ATTRIBUTES_FACTORY.apply(req, res));
// 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
// ...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,
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).
*
* @param context
- * the servlet context
+ * the servlet context
* @param req
- * the servlet request
+ * the servlet request
* @param res
- * the servlet response
+ * the servlet response
* @param chain
- * the filter chain
+ * the filter chain
* @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
- * 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,
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.
*
* @param context
- * the servlet context
+ * the servlet context
* @param req
- * the servlet request
+ * the servlet request
* @param res
- * the servlet response
+ * the servlet response
* @param chain
- * the filter chain
+ * the filter chain
* @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
- * the Keycloak HTTP facade
+ * the Keycloak HTTP facade
* @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
- * 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,
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.
*
* @param context
- * the servlet context
+ * the servlet context
* @param req
- * the servlet request
+ * the servlet request
* @param res
- * the servlet response
+ * the servlet response
* @param chain
- * the filter chain
+ * the filter chain
* @param bodyBufferLimit
- * the configured size limit to apply to any HTTP POST/PUT body buffering that may need to be applied to process the
- * authentication via an intermediary redirect
+ * the configured size limit to apply to any HTTP POST/PUT body buffering that may need to be applied to process the
+ * authentication via an intermediary redirect
* @param facade
- * the Keycloak HTTP facade
+ * the Keycloak HTTP facade
* @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
- * 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,
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.
*
* @param context
- * the servlet context
+ * the servlet context
* @param req
- * the HTTP servlet request being processed
+ * the HTTP servlet request being processed
* @param res
- * the HTTP servlet response being processed
+ * the HTTP servlet response being processed
* @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,
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.
*
* @param context
- * the servlet context
+ * the servlet context
* @param req
- * the HTTP servlet request being processed
+ * the HTTP servlet request being processed
* @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)
{
@@ -877,21 +920,21 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Processes a successful authentication via Keycloak.
*
* @param context
- * the servlet context
+ * the servlet context
* @param req
- * the servlet request
+ * the servlet request
* @param res
- * the servlet response
+ * the servlet response
* @param chain
- * the filter chain
+ * the filter chain
* @param facade
- * the Keycloak HTTP facade
+ * the Keycloak HTTP facade
* @param tokenStore
- * the Keycloak token store
+ * the Keycloak token store
* @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
- * 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,
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.
*
* @param context
- * the servlet context
+ * the servlet context
* @param req
- * the servlet request
+ * the servlet request
* @param res
- * the servlet response
+ * the servlet response
* @param chain
- * the filter chain
+ * the filter chain
* @param facade
- * the Keycloak HTTP facade
+ * the Keycloak HTTP facade
* @param tokenHolder
- * the holder for access token taken from the successful authentication
+ * the holder for access token taken from the successful authentication
* @throws IOException
- * if any error occurs during Keycloak authentication or processing of the filter chain
+ * if any error occurs during Keycloak authentication or processing of the filter chain
* @throws ServletException
- * if any error occurs during Keycloak authentication or processing of the filter chain
+ * if any error occurs during Keycloak authentication or processing of the filter chain
*/
protected void onKeycloakAuthenticationSuccess(final ServletContext context, final HttpServletRequest req,
final HttpServletResponse res, final FilterChain chain, final OIDCServletHttpFacade facade,
@@ -1033,17 +1076,17 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Processes a failed authentication via Keycloak.
*
* @param context
- * the servlet context
+ * the servlet context
* @param req
- * the servlet request
+ * the servlet request
* @param res
- * the servlet response
+ * the servlet response
* @param chain
- * the filter chain
+ * the filter chain
* @throws IOException
- * if any error occurs during processing of the filter chain
+ * if any error occurs during processing of the filter chain
* @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,
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.
*
* @param req
- * the servlet request
+ * the servlet request
* @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
{
@@ -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.
*
* @param context
- * the servlet context
+ * the servlet context
* @param request
- * the current request
+ * the current request
* @param response
- * the response to the current request
+ * the response to the current request
* @param chain
- * the filter chain
+ * the filter chain
* @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
- * 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,
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.
*
* @param req
- * the servlet request to check for potential conditions to skip
+ * the servlet request to check for potential conditions to skip
* @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
- * operation} must be skipped, {@code false} otherwise
+ * operation} must be skipped, {@code false} otherwise
* @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
{
@@ -1337,15 +1380,15 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* necessary or configured.
*
* @param req
- * the HTTP servlet request
+ * the HTTP servlet request
* @param res
- * the HTTP servlet response
+ * the HTTP servlet response
* @param userId
- * the ID of the authenticated user
+ * the ID of the authenticated user
* @param keycloakAccount
- * the Keycloak account object
+ * the Keycloak account object
* @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,
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.
*
* @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,
- * {@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
- * 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
{
@@ -1422,11 +1465,11 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Checks if the requested page is a login page.
*
* @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,
- * {@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
- * 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
{
@@ -1472,11 +1515,11 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Checks if the requested URL indicates a logout request.
*
* @param req
- * the request to check
+ * the request to check
* @return {@code true} if the request is a request for logout,
- * {@code false} otherwise
+ * {@code false} otherwise
* @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
{
@@ -1491,7 +1534,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Checks if the HTTP request has set the Keycloak state cookie.
*
* @param req
- * the HTTP request to check
+ * the HTTP request to check
* @return {@code true} if the state cookie is set, {@code false} otherwise
*/
protected boolean hasStateCookie(final HttpServletRequest req)
@@ -1509,9 +1552,9 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* authenticated user.
*
* @param req
- * the request to check
+ * the request to check
* @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
*/
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.
*
* @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)
{
@@ -1685,12 +1728,12 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* that backend resource.
*
* @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
- * 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
* @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
{
@@ -1766,11 +1809,11 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
* Resets any Keycloak-related state cookies present in the current request.
*
* @param context
- * the servlet context
+ * the servlet context
* @param req
- * the servlet request
+ * the servlet request
* @param res
- * the servlet response
+ * the servlet response
*/
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.
*
* @param req
- * the incoming request
+ * the incoming request
* @return the assumed SSL port to be used in redirects
*/
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).
*
* @param configElement
- * the adapter configuration
+ * the adapter configuration
* @param client
- * the client to configure
+ * the client to configure
*/
@SuppressWarnings("deprecation")
protected void configureForcedRouteIfNecessary(final KeycloakAdapterConfigElement configElement, final HttpClient client)
diff --git a/share/src/test/docker/alfresco/extension/alfresco-global.addition.properties b/share/src/test/docker/alfresco/extension/alfresco-global.addition.properties
index 8a39456..b68d867 100644
--- a/share/src/test/docker/alfresco/extension/alfresco-global.addition.properties
+++ b/share/src/test/docker/alfresco/extension/alfresco-global.addition.properties
@@ -18,7 +18,7 @@
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.resource=alfresco
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.groupFilter.containedInGroup.property.groupPaths=/Test A
+
+keycloak.synchronization.requiredClientScopes=realm-management
\ No newline at end of file
diff --git a/share/src/test/docker/test-realm.json b/share/src/test/docker/test-realm.json
index 3dd1d7c..8a78791 100644
--- a/share/src/test/docker/test-realm.json
+++ b/share/src/test/docker/test-realm.json
@@ -10,18 +10,552 @@
"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": [
{
"id": "alfresco",
"clientId": "alfresco",
"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",
+ "baseUrl": "/",
"redirectUris": [
- "http://localhost:${docker.tests.repositoryPort}/alfresco/*"
+ "/*"
],
"webOrigins": [
- "http://localhost:${docker.tests.repositoryPort}/alfresco"
+ "/"
],
"enabled": true,
"clientAuthenticatorType": "client-secret",
@@ -29,25 +563,41 @@
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": true,
"publicClient": false,
- "protocol": "openid-connect"
+ "protocol": "openid-connect",
+ "defaultClientScopes": [
+ "profile",
+ "email",
+ "address",
+ "phone",
+ "realm-roles",
+ "alfresco"
+ ],
+ "optionalClientScopes": [
+ "realm-management"
+ ]
},
{
"id": "alfresco-share",
"clientId": "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",
+ "baseUrl": "/",
"redirectUris": [
- "http://localhost:${docker.tests.sharePort}/share/*"
+ "/*"
],
"webOrigins": [
- "http://localhost:${docker.tests.sharePort}/share"
+ "/"
],
"enabled": true,
"clientAuthenticatorType": "client-secret",
"secret": "a5b3e8bc-39cc-4ddd-8c8f-1c34e7a35975",
"publicClient": false,
- "protocol": "openid-connect"
+ "protocol": "openid-connect",
+ "defaultClientScopes": [
+ "realm-roles",
+ "alfresco-share"
+ ]
},
{
"clientId": "realm-management",
@@ -250,6 +800,8 @@
"manage-account"
],
"realm-management": [
+ "query-groups",
+ "query-users",
"view-users",
"view-clients"
]