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" ]