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 e9333379fb..300329a509 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 21758a43cb..8d4ed90d34 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"); }