diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java
index e933337..300329a 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java
@@ -82,6 +82,9 @@ abstract class AbstractUserRoleMappingMapper extends AbstractOIDCProtocolMapper
private static final Pattern CLIENT_ID_PATTERN = Pattern.compile("\\$\\{client_id\\}");
+ private static final Pattern DOT_PATTERN = Pattern.compile("\\.");
+ private static final String DOT_REPLACEMENT = "\\\\\\\\.";
+
private static void mapClaim(IDToken token, ProtocolMapperModel mappingModel, Object attributeValue, String clientId) {
attributeValue = OIDCAttributeMapperHelper.mapAttributeValue(mappingModel, attributeValue);
if (attributeValue == null) return;
@@ -92,6 +95,8 @@ abstract class AbstractUserRoleMappingMapper extends AbstractOIDCProtocolMapper
}
if (clientId != null) {
+ // case when clientId contains dots
+ clientId = DOT_PATTERN.matcher(clientId).replaceAll(DOT_REPLACEMENT);
protocolClaim = CLIENT_ID_PATTERN.matcher(protocolClaim).replaceAll(clientId);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java
index 21758a4..8d4ed90 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java
@@ -22,6 +22,8 @@ import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.util.PemUtils;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
@@ -29,6 +31,8 @@ import org.keycloak.events.EventType;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.testsuite.util.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
@@ -42,6 +46,7 @@ import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.util.ClientManager;
+import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.TokenSignatureUtil;
import org.keycloak.testsuite.util.UserInfoClientUtil;
@@ -60,6 +65,7 @@ import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import java.net.URI;
import java.security.PublicKey;
+import java.util.Collections;
import java.util.List;
import static org.junit.Assert.assertEquals;
@@ -152,6 +158,51 @@ public class UserInfoTest extends AbstractKeycloakTest {
}
}
+
+ // KEYCLOAK-8838
+ @Test
+ public void testSuccess_dotsInClientId() throws Exception {
+ // Create client with dot in the name and with some role
+ ClientRepresentation clientRep = org.keycloak.testsuite.util.ClientBuilder.create()
+ .clientId("my.foo.client")
+ .addRedirectUri("http://foo.host")
+ .secret("password")
+ .directAccessGrants()
+ .defaultRoles("my.foo.role")
+ .build();
+
+ RealmResource realm = adminClient.realm("test");
+
+ Response resp = realm.clients().create(clientRep);
+ String clientUUID = ApiUtil.getCreatedId(resp);
+ resp.close();
+ getCleanup().addClientUuid(clientUUID);
+
+ // Assign role to the user
+ RoleRepresentation fooRole = realm.clients().get(clientUUID).roles().get("my.foo.role").toRepresentation();
+ UserResource userResource = ApiUtil.findUserByUsernameId(realm, "test-user@localhost");
+ userResource.roles().clientLevel(clientUUID).add(Collections.singletonList(fooRole));
+
+ // Login to the new client
+ OAuthClient.AccessTokenResponse accessTokenResponse = oauth.clientId("my.foo.client")
+ .doGrantAccessTokenRequest("password", "test-user@localhost", "password");
+
+ AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken());
+ Assert.assertNames(accessToken.getResourceAccess("my.foo.client").getRoles(), "my.foo.role");
+
+ events.clear();
+
+ // Send UserInfo request and ensure it is correct
+ Client client = ClientBuilder.newClient();
+ try {
+ Response response = UserInfoClientUtil.executeUserInfoRequest_getMethod(client, accessTokenResponse.getAccessToken());
+
+ testSuccessfulUserInfoResponse(response, "my.foo.client");
+ } finally {
+ client.close();
+ }
+ }
+
@Test
public void testSuccess_postMethod_header_textEntity() throws Exception {
Client client = ClientBuilder.newClient();
@@ -418,11 +469,16 @@ public class UserInfoTest extends AbstractKeycloakTest {
}
private void testSuccessfulUserInfoResponse(Response response) {
+ testSuccessfulUserInfoResponse(response, "test-app");
+ }
+
+ private void testSuccessfulUserInfoResponse(Response response, String expectedClientId) {
events.expect(EventType.USER_INFO_REQUEST)
.session(Matchers.notNullValue(String.class))
.detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN)
.detail(Details.USERNAME, "test-user@localhost")
.detail(Details.SIGNATURE_REQUIRED, "false")
+ .client(expectedClientId)
.assertEvent();
UserInfoClientUtil.testSuccessfulUserInfoResponse(response, "test-user@localhost", "test-user@localhost");
}