Test and deal with granular client scopes

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

View File

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

View File

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

View File

@ -288,7 +288,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
String realUserName = userName; String realUserName = userName;
try try
{ {
accessTokenHolder = this.accessTokenClient.obtainAccessToken(userName, new String(password)); accessTokenHolder = this.accessTokenClient.obtainAccessToken(userName, new String(password), Collections.emptySet());
realUserName = accessTokenHolder.getAccessToken().getPreferredUsername(); realUserName = accessTokenHolder.getAccessToken().getPreferredUsername();
// for potential one-off authentication, we do not care particularly about the token TTL - so no validation here // for potential one-off authentication, we do not care particularly about the token TTL - so no validation here

View File

@ -21,6 +21,9 @@ import com.fasterxml.jackson.databind.MappingIterator;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -65,6 +68,8 @@ public class IDMClientImpl implements InitializingBean, IDMClient
protected String password; protected String password;
protected final Collection<String> requiredClientScopes = new HashSet<>();
protected AccessTokenHolder accessToken; protected AccessTokenHolder accessToken;
/** /**
@ -113,6 +118,19 @@ public class IDMClientImpl implements InitializingBean, IDMClient
this.password = password; this.password = password;
} }
/**
* @param requiredClientScopes
* the requiredClientScopes to set
*/
public void setRequiredClientScopes(final String requiredClientScopes)
{
this.requiredClientScopes.clear();
if (requiredClientScopes != null && !requiredClientScopes.isEmpty())
{
this.requiredClientScopes.addAll(Arrays.asList(requiredClientScopes.trim().split(" ")));
}
}
/** /**
* *
* {@inheritDoc} * {@inheritDoc}
@ -610,11 +628,12 @@ public class IDMClientImpl implements InitializingBean, IDMClient
{ {
if (this.userName != null && !this.userName.isEmpty()) if (this.userName != null && !this.userName.isEmpty())
{ {
this.accessToken = this.accessTokenService.obtainAccessToken(this.userName, this.password); this.accessToken = this.accessTokenService.obtainAccessToken(this.userName, this.password,
this.requiredClientScopes);
} }
else else
{ {
this.accessToken = this.accessTokenService.obtainAccessToken(); this.accessToken = this.accessTokenService.obtainAccessToken(this.requiredClientScopes);
} }
} }
} }

View File

@ -3,6 +3,7 @@ package de.acosix.alfresco.keycloak.repo.token;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -57,17 +58,21 @@ public class AccessTokenClient
* Obtains an access token for the service account of the client used to integrate this Alfresco isntance with Keycloak. This requires * Obtains an access token for the service account of the client used to integrate this Alfresco isntance with Keycloak. This requires
* that a service account has been enabled / configured in Keycloak. * that a service account has been enabled / configured in Keycloak.
* *
* @param scopes
* the optional scopes to request for the access token
* @return the access token * @return the access token
* @throws AccessTokenException * @throws AccessTokenException
* if the access token cannot be obtained * if the access token cannot be obtained
*/ */
public RefreshableAccessTokenHolder obtainAccessToken() public RefreshableAccessTokenHolder obtainAccessToken(final Collection<String> scopes)
{ {
LOGGER.debug("Obtaining client access token"); ParameterCheck.mandatory("scopes", scopes);
LOGGER.debug("Obtaining client access token with (optional) scopes {}", scopes);
try try
{ {
final AccessTokenResponse response = this.getAccessTokenImpl(formParams -> { final AccessTokenResponse response = this.getAccessTokenImpl(formParams -> {
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
this.processScopes(scopes, formParams);
}); });
final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response); final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response);
final RefreshableAccessTokenHolder refreshableToken = new RefreshableAccessTokenHolder(response, verifiedTokens); final RefreshableAccessTokenHolder refreshableToken = new RefreshableAccessTokenHolder(response, verifiedTokens);
@ -88,19 +93,26 @@ public class AccessTokenClient
* the name of the user * the name of the user
* @param password * @param password
* the password provided by / for the user * the password provided by / for the user
* @param scopes
* the optional scopes to request for the access token
* @return the access token * @return the access token
* @throws AccessTokenException * @throws AccessTokenException
* if the access token cannot be obtained * if the access token cannot be obtained
*/ */
public RefreshableAccessTokenHolder obtainAccessToken(final String user, final String password) public RefreshableAccessTokenHolder obtainAccessToken(final String user, final String password, final Collection<String> scopes)
{ {
LOGGER.debug("Obtaining access token for user {}", user); ParameterCheck.mandatoryString("user", user);
ParameterCheck.mandatoryString("password", password);
ParameterCheck.mandatory("scopes", scopes);
LOGGER.debug("Obtaining access token for user {} with (optional) scopes {}", user, scopes);
try try
{ {
final AccessTokenResponse response = this.getAccessTokenImpl(formParams -> { final AccessTokenResponse response = this.getAccessTokenImpl(formParams -> {
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD)); formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
formParams.add(new BasicNameValuePair("username", user)); formParams.add(new BasicNameValuePair("username", user));
formParams.add(new BasicNameValuePair("password", password)); formParams.add(new BasicNameValuePair("password", password));
this.processScopes(scopes, formParams);
}); });
final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response); final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response);
final RefreshableAccessTokenHolder refreshableToken = new RefreshableAccessTokenHolder(response, verifiedTokens); final RefreshableAccessTokenHolder refreshableToken = new RefreshableAccessTokenHolder(response, verifiedTokens);
@ -121,19 +133,26 @@ public class AccessTokenClient
* the access token to exchange * the access token to exchange
* @param client * @param client
* the client / service for which to obtain an access token * the client / service for which to obtain an access token
* @param scopes
* the optional scopes to request for the access token
* @return the access token to the requested client / service * @return the access token to the requested client / service
* @throws AccessTokenException * @throws AccessTokenException
* if the token cannot be exchanged * if the token cannot be exchanged
*/ */
public RefreshableAccessTokenHolder exchangeToken(final String accessToken, final String client) public RefreshableAccessTokenHolder exchangeToken(final String accessToken, final String client, final Collection<String> scopes)
{ {
LOGGER.debug("Exchanging {} for access token to client {}", accessToken, client); ParameterCheck.mandatoryString("accessToken", accessToken);
ParameterCheck.mandatoryString("client", client);
ParameterCheck.mandatory("scopes", scopes);
LOGGER.debug("Exchanging {} for access token to client {} with (optional) scopes {}", accessToken, client, scopes);
try try
{ {
final AccessTokenResponse response = this.getAccessTokenImpl(formParams -> { final AccessTokenResponse response = this.getAccessTokenImpl(formParams -> {
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)); formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE));
formParams.add(new BasicNameValuePair(OAuth2Constants.AUDIENCE, client)); formParams.add(new BasicNameValuePair(OAuth2Constants.AUDIENCE, client));
formParams.add(new BasicNameValuePair(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.REFRESH_TOKEN_TYPE)); formParams.add(new BasicNameValuePair(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.REFRESH_TOKEN_TYPE));
this.processScopes(scopes, formParams);
}); });
final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response, client); final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response, client);
final RefreshableAccessTokenHolder refreshableToken = new RefreshableAccessTokenHolder(response, verifiedTokens); final RefreshableAccessTokenHolder refreshableToken = new RefreshableAccessTokenHolder(response, verifiedTokens);
@ -178,6 +197,23 @@ public class AccessTokenClient
} }
} }
protected void processScopes(final Collection<String> scopes, final List<NameValuePair> formParams)
{
if (!scopes.isEmpty())
{
final StringBuilder sb = new StringBuilder(scopes.size() * 16);
for (final String scope : scopes)
{
if (sb.length() > 0)
{
sb.append(' ');
}
sb.append(scope);
}
formParams.add(new BasicNameValuePair(OAuth2Constants.SCOPE, sb.toString()));
}
}
protected VerifiedTokens verifyAccessTokenResponse(final AccessTokenResponse response) protected VerifiedTokens verifyAccessTokenResponse(final AccessTokenResponse response)
{ {
final VerifiedTokens tokens; final VerifiedTokens tokens;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,8 @@ import static org.alfresco.web.site.SlingshotPageView.REDIRECT_URI;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -27,6 +29,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -177,6 +180,37 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
private static final ThreadLocal<String> LOGIN_REDIRECT_URL = new ThreadLocal<>(); private static final ThreadLocal<String> LOGIN_REDIRECT_URL = new ThreadLocal<>();
private static final BiFunction<HttpServletRequest, HttpServletResponse, ServletRequestAttributes> SERVLET_REQUEST_ATTRIBUTES_FACTORY;
static
{
BiFunction<HttpServletRequest, HttpServletResponse, ServletRequestAttributes> factory;
try
{
// try and use the overloaded constructor available in newer versions
final Constructor<ServletRequestAttributes> ctor = ServletRequestAttributes.class.getConstructor(HttpServletRequest.class,
HttpServletResponse.class);
factory = (req, res) -> {
try
{
return ctor.newInstance(req, res);
}
catch (final InstantiationException | IllegalAccessException | InvocationTargetException e)
{
throw new AlfrescoRuntimeException("Failed to construct servlet request attributes", e);
}
};
}
catch (final NoSuchMethodException nsme)
{
// fallback to constructor that's available in all Share versions
factory = (req, res) -> new ServletRequestAttributes(req);
}
SERVLET_REQUEST_ATTRIBUTES_FACTORY = factory;
}
protected ApplicationContext applicationContext; protected ApplicationContext applicationContext;
protected DependencyInjectedFilter defaultSsoFilter; protected DependencyInjectedFilter defaultSsoFilter;
@ -265,28 +299,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
final RemoteConfigElement remoteConfig = (RemoteConfigElement) this.configService.getConfig("Remote").getConfigElement("remote"); final RemoteConfigElement remoteConfig = (RemoteConfigElement) this.configService.getConfig("Remote").getConfigElement("remote");
if (remoteConfig != null) if (remoteConfig != null)
{ {
final EndpointDescriptor endpoint = remoteConfig.getEndpointDescriptor(this.primaryEndpoint); this.initFromRemoteEndpointConfig(remoteConfig);
if (endpoint != null)
{
this.externalAuthEnabled = endpoint.getExternalAuth();
}
else
{
LOGGER.error("Endpoint {} has not been defined in the application configuration", this.primaryEndpoint);
}
if (this.secondaryEndpoints != null)
{
this.secondaryEndpoints = this.secondaryEndpoints.stream().filter(secondaryEndpoint -> {
final boolean endpointExists = remoteConfig.getEndpointDescriptor(secondaryEndpoint) != null;
if (!endpointExists)
{
LOGGER.info("Excluding configured secondary endpoint {} which is not defined in the application configuration",
secondaryEndpoint);
}
return endpointExists;
}).collect(Collectors.toList());
}
} }
else else
{ {
@ -297,37 +310,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
.getConfig(KeycloakConfigConstants.KEYCLOAK_CONFIG_SECTION_NAME).getConfigElement(KeycloakAdapterConfigElement.NAME); .getConfig(KeycloakConfigConstants.KEYCLOAK_CONFIG_SECTION_NAME).getConfigElement(KeycloakAdapterConfigElement.NAME);
if (keycloakAdapterConfig != null) if (keycloakAdapterConfig != null)
{ {
final AdapterConfig adapterConfiguration = keycloakAdapterConfig.buildAdapterConfiguration(); this.initFromAdapterConfig(keycloakAdapterConfig);
// disable any CORS handling (if CORS is relevant, it should be handled by Share / Surf)
adapterConfiguration.setCors(false);
// BASIC authentication should never be used
adapterConfiguration.setEnableBasicAuth(false);
this.keycloakDeployment = KeycloakDeploymentBuilder.build(adapterConfiguration);
// even in newer version than used by ACS 6.x does Keycloak lib not allow timeout configuration
if (this.keycloakDeployment.getClient() != null)
{
final Long connectionTimeout = keycloakAdapterConfig.getConnectionTimeout();
final Long socketTimeout = keycloakAdapterConfig.getSocketTimeout();
HttpClientBuilder httpClientBuilder = new HttpClientBuilder();
if (connectionTimeout != null && connectionTimeout.longValue() >= 0)
{
httpClientBuilder = httpClientBuilder.establishConnectionTimeout(connectionTimeout.longValue(), TimeUnit.MILLISECONDS);
}
if (socketTimeout != null && socketTimeout.longValue() >= 0)
{
httpClientBuilder = httpClientBuilder.socketTimeout(socketTimeout.longValue(), TimeUnit.MILLISECONDS);
}
final HttpClient client = httpClientBuilder.build(adapterConfiguration);
this.configureForcedRouteIfNecessary(keycloakAdapterConfig, client);
this.keycloakDeployment.setClient(client);
}
this.deploymentContext = new AdapterDeploymentContext(this.keycloakDeployment);
} }
else else
{ {
@ -439,8 +422,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
this.keycloakDeployment.getAuthServerBaseUrl()); this.keycloakDeployment.getAuthServerBaseUrl());
} }
// TODO Figure out how to support Enteprise 6.2 / 7.x or 6.3+, which overload the constructor RequestContextHolder.setRequestAttributes(SERVLET_REQUEST_ATTRIBUTES_FACTORY.apply(req, res));
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(req));
// Alfresco handling of RequestContext / ServletUtil / any other context holder is so immensely broken, it isn't even funny // Alfresco handling of RequestContext / ServletUtil / any other context holder is so immensely broken, it isn't even funny
// this request context is for any handling that needs it until it gets nuked / bulldozed by RequestContextInterceptor // this request context is for any handling that needs it until it gets nuked / bulldozed by RequestContextInterceptor
// ...after which we will have to enhance that class' partially initialised context // ...after which we will have to enhance that class' partially initialised context
@ -501,6 +483,67 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
} }
} }
protected void initFromRemoteEndpointConfig(final RemoteConfigElement remoteConfig)
{
final EndpointDescriptor endpoint = remoteConfig.getEndpointDescriptor(this.primaryEndpoint);
if (endpoint != null)
{
this.externalAuthEnabled = endpoint.getExternalAuth();
}
else
{
LOGGER.error("Endpoint {} has not been defined in the application configuration", this.primaryEndpoint);
}
if (this.secondaryEndpoints != null)
{
this.secondaryEndpoints = this.secondaryEndpoints.stream().filter(secondaryEndpoint -> {
final boolean endpointExists = remoteConfig.getEndpointDescriptor(secondaryEndpoint) != null;
if (!endpointExists)
{
LOGGER.info("Excluding configured secondary endpoint {} which is not defined in the application configuration",
secondaryEndpoint);
}
return endpointExists;
}).collect(Collectors.toList());
}
}
protected void initFromAdapterConfig(final KeycloakAdapterConfigElement keycloakAdapterConfig)
{
final AdapterConfig adapterConfiguration = keycloakAdapterConfig.buildAdapterConfiguration();
// disable any CORS handling (if CORS is relevant, it should be handled by Share / Surf)
adapterConfiguration.setCors(false);
// BASIC authentication should never be used
adapterConfiguration.setEnableBasicAuth(false);
this.keycloakDeployment = KeycloakDeploymentBuilder.build(adapterConfiguration);
// even in newer version than used by ACS 6.x does Keycloak lib not allow timeout configuration
if (this.keycloakDeployment.getClient() != null)
{
final Long connectionTimeout = keycloakAdapterConfig.getConnectionTimeout();
final Long socketTimeout = keycloakAdapterConfig.getSocketTimeout();
HttpClientBuilder httpClientBuilder = new HttpClientBuilder();
if (connectionTimeout != null && connectionTimeout.longValue() >= 0)
{
httpClientBuilder = httpClientBuilder.establishConnectionTimeout(connectionTimeout.longValue(), TimeUnit.MILLISECONDS);
}
if (socketTimeout != null && socketTimeout.longValue() >= 0)
{
httpClientBuilder = httpClientBuilder.socketTimeout(socketTimeout.longValue(), TimeUnit.MILLISECONDS);
}
final HttpClient client = httpClientBuilder.build(adapterConfiguration);
this.configureForcedRouteIfNecessary(keycloakAdapterConfig, client);
this.keycloakDeployment.setClient(client);
}
this.deploymentContext = new AdapterDeploymentContext(this.keycloakDeployment);
}
protected void processLogout(final ServletContext context, final HttpServletRequest req, final HttpServletResponse res, protected void processLogout(final ServletContext context, final HttpServletRequest req, final HttpServletResponse res,
final FilterChain chain) throws IOException, ServletException final FilterChain chain) throws IOException, ServletException
{ {

View File

@ -18,7 +18,7 @@
authentication.chain=keycloak1:keycloak,alfrescoNtlm1:alfrescoNtlm authentication.chain=keycloak1:keycloak,alfrescoNtlm1:alfrescoNtlm
keycloak.adapter.auth-server-url=http://${docker.tests.host.name}:${docker.tests.keycloakPort}/auth keycloak.adapter.auth-server-url=http://localhost:${docker.tests.keycloakPort}/auth
keycloak.adapter.realm=test keycloak.adapter.realm=test
keycloak.adapter.resource=alfresco keycloak.adapter.resource=alfresco
keycloak.adapter.credentials.provider=secret keycloak.adapter.credentials.provider=secret
@ -29,3 +29,5 @@ keycloak.adapter.directAuthHost=http://keycloak:8080
keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=/Test A keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=/Test A
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=/Test A keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=/Test A
keycloak.synchronization.requiredClientScopes=realm-management

View File

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