Test and deal with granular client scopes

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

View File

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

View File

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

View File

@@ -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)
{

View File

@@ -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<String> 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 <T>
* 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 <T> int processEntityBatch(final URI uri, final Consumer<T> entityProcessor, final Class<T> entityClass)
@@ -483,9 +501,9 @@ public class IDMClientImpl implements InitializingBean, IDMClient
* Executes a generic HTTP GET operation yielding a JSON response.
*
* @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<JsonNode> responseProcessor)
{
@@ -539,11 +557,11 @@ public class IDMClientImpl implements InitializingBean, IDMClient
* Executes a generic HTTP GET operation yielding a mapped response entity.
*
* @param <T>
* 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);
}
}
}

View File

@@ -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<String> 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<String> scopes)
{
LOGGER.debug("Obtaining access token for user {}", user);
ParameterCheck.mandatoryString("user", user);
ParameterCheck.mandatoryString("password", password);
ParameterCheck.mandatory("scopes", scopes);
LOGGER.debug("Obtaining access token for user {} with (optional) scopes {}", user, scopes);
try
{
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<String> scopes)
{
LOGGER.debug("Exchanging {} for access token to client {}", accessToken, client);
ParameterCheck.mandatoryString("accessToken", accessToken);
ParameterCheck.mandatoryString("client", client);
ParameterCheck.mandatory("scopes", scopes);
LOGGER.debug("Exchanging {} for access token to client {} with (optional) scopes {}", accessToken, client, scopes);
try
{
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<String> scopes, final List<NameValuePair> formParams)
{
if (!scopes.isEmpty())
{
final StringBuilder sb = new StringBuilder(scopes.size() * 16);
for (final String scope : scopes)
{
if (sb.length() > 0)
{
sb.append(' ');
}
sb.append(scope);
}
formParams.add(new BasicNameValuePair(OAuth2Constants.SCOPE, sb.toString()));
}
}
protected VerifiedTokens verifyAccessTokenResponse(final AccessTokenResponse response)
{
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

View File

@@ -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<String> 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<String> scopes);
/**
* Performs a token exchange operation to obtain an access token for a specific client, acting in the name / context of the user for
* 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<String> scopes);
}

View File

@@ -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<String> scopes)
{
final RefreshableAccessTokenHolder refreshableToken = this.accessTokenClient.obtainAccessToken();
ParameterCheck.mandatory("scopes", scopes);
final RefreshableAccessTokenHolder refreshableToken = this.accessTokenClient.obtainAccessToken(scopes);
return new AccessTokenHolderImpl(refreshableToken, this.deployment.getTokenMinimumTimeToLive(),
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<String> 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<String> 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)
{

View File

@@ -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<String> 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<String> 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<String> scopes)
{
throw new AccessTokenUnsupportedException(UNSUPPORTED_MESSAGE);
}

View File

@@ -29,3 +29,5 @@ keycloak.adapter.directAuthHost=http://keycloak:8080
keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=/Test A
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=/Test A
keycloak.synchronization.requiredClientScopes=realm-management

View File

@@ -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": {