/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.oauth;
import org.hamcrest.CoreMatchers;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ProtocolMappersResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.AddressMapper;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AddressClaimSet;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
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.ProtocolMapperUtil;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId;
import static org.keycloak.testsuite.admin.ApiUtil.findClientResourceByClientId;
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsernameId;
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createAddressMapper;
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createClaimMapper;
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createHardcodedClaim;
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createHardcodedRole;
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createRoleNameMapper;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
@Rule
public AssertEvents events = new AssertEvents(this);
@Override
public void beforeAbstractKeycloakTest() throws Exception {
super.beforeAbstractKeycloakTest();
}
@Before
public void clientConfiguration() {
ClientManager.realm(adminClient.realm("test")).clientId("test-app").directAccessGrant(true);
/*
* Configure the default client ID. Seems like OAuthClient is keeping the state of clientID
* For example: If some test case configure oauth.clientId("sample-public-client"), other tests
* will faile and the clientID will always be "sample-public-client
* @see AccessTokenTest#testAuthorizationNegotiateHeaderIgnored()
*/
oauth.clientId("test-app");
}
private void deleteMappers(ProtocolMappersResource protocolMappers) {
ProtocolMapperRepresentation mapper = ProtocolMapperUtil.getMapperByNameAndProtocol(protocolMappers, OIDCLoginProtocol.LOGIN_PROTOCOL, "Realm roles mapper");
protocolMappers.delete(mapper.getId());
mapper = ProtocolMapperUtil.getMapperByNameAndProtocol(protocolMappers, OIDCLoginProtocol.LOGIN_PROTOCOL, "Client roles mapper");
protocolMappers.delete(mapper.getId());
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
testRealms.add(realm);
}
@Test
public void testTokenMapping() throws Exception {
{
UserResource userResource = findUserByUsernameId(adminClient.realm("test"), "test-user@localhost");
UserRepresentation user = userResource.toRepresentation();
user.singleAttribute("street", "5 Yawkey Way");
user.singleAttribute("locality", "Boston");
user.singleAttribute("region_some", "MA"); // Custom name for userAttribute name, which will be mapped to region
user.singleAttribute("postal_code", "02115");
user.singleAttribute("country", "USA");
user.singleAttribute("formatted", "6 Foo Street");
user.singleAttribute("phone", "617-777-6666");
List<String> departments = Arrays.asList("finance", "development");
user.getAttributes().put("departments", departments);
userResource.update(user);
ClientResource app = findClientResourceByClientId(adminClient.realm("test"), "test-app");
ProtocolMapperRepresentation mapper = createAddressMapper(true, true);
mapper.getConfig().put(AddressMapper.getModelPropertyName(AddressClaimSet.REGION), "region_some");
mapper.getConfig().put(AddressMapper.getModelPropertyName(AddressClaimSet.COUNTRY), "country_some");
mapper.getConfig().remove(AddressMapper.getModelPropertyName(AddressClaimSet.POSTAL_CODE)); // Even if we remove protocolMapper config property, it should still default to postal_code
app.getProtocolMappers().createMapper(mapper).close();
ProtocolMapperRepresentation hard = createHardcodedClaim("hard", "hard", "coded", "String", false, null, true, true);
app.getProtocolMappers().createMapper(hard).close();
app.getProtocolMappers().createMapper(createHardcodedClaim("hard-nested", "nested.hard", "coded-nested", "String", false, null, true, true)).close();
app.getProtocolMappers().createMapper(createClaimMapper("custom phone", "phone", "home_phone", "String", true, "", true, true, true)).close();
app.getProtocolMappers().createMapper(createClaimMapper("nested phone", "phone", "home.phone", "String", true, "", true, true, true)).close();
app.getProtocolMappers().createMapper(createClaimMapper("departments", "departments", "department", "String", true, "", true, true, true)).close();
app.getProtocolMappers().createMapper(createClaimMapper("firstDepartment", "departments", "firstDepartment", "String", true, "", true, true, false)).close();
app.getProtocolMappers().createMapper(createHardcodedRole("hard-realm", "hardcoded")).close();
app.getProtocolMappers().createMapper(createHardcodedRole("hard-app", "app.hardcoded")).close();
app.getProtocolMappers().createMapper(createRoleNameMapper("rename-app-role", "test-app.customer-user", "realm-user")).close();
}
{
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password");
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
assertNotNull(idToken.getAddress());
assertEquals(idToken.getName(), "Tom Brady");
assertEquals(idToken.getAddress().getStreetAddress(), "5 Yawkey Way");
assertEquals(idToken.getAddress().getLocality(), "Boston");
assertEquals(idToken.getAddress().getRegion(), "MA");
assertEquals(idToken.getAddress().getPostalCode(), "02115");
assertNull(idToken.getAddress().getCountry()); // Null because we changed userAttribute name to "country_some", but user contains "country"
assertEquals(idToken.getAddress().getFormattedAddress(), "6 Foo Street");
assertNotNull(idToken.getOtherClaims().get("home_phone"));
assertThat((List<String>) idToken.getOtherClaims().get("home_phone"), hasItems("617-777-6666"));
assertEquals("coded", idToken.getOtherClaims().get("hard"));
Map nested = (Map) idToken.getOtherClaims().get("nested");
assertEquals("coded-nested", nested.get("hard"));
nested = (Map) idToken.getOtherClaims().get("home");
assertThat((List<String>) nested.get("phone"), hasItems("617-777-6666"));
List<String> departments = (List<String>) idToken.getOtherClaims().get("department");
assertThat(departments, containsInAnyOrder("finance", "development"));
Object firstDepartment = idToken.getOtherClaims().get("firstDepartment");
assertThat(firstDepartment, instanceOf(String.class));
assertThat(firstDepartment, anyOf(is("finance"), is("development"))); // Has to be the first item
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
assertEquals(accessToken.getName(), "Tom Brady");
assertNotNull(accessToken.getAddress());
assertEquals(accessToken.getAddress().getStreetAddress(), "5 Yawkey Way");
assertEquals(accessToken.getAddress().getLocality(), "Boston");
assertEquals(accessToken.getAddress().getRegion(), "MA");
assertEquals(accessToken.getAddress().getPostalCode(), "02115");
assertNull(idToken.getAddress().getCountry()); // Null because we changed userAttribute name to "country_some", but user contains "country"
assertEquals(idToken.getAddress().getFormattedAddress(), "6 Foo Street");
assertNotNull(accessToken.getOtherClaims().get("home_phone"));
assertThat((List<String>) accessToken.getOtherClaims().get("home_phone"), hasItems("617-777-6666"));
assertEquals("coded", accessToken.getOtherClaims().get("hard"));
nested = (Map) accessToken.getOtherClaims().get("nested");
assertEquals("coded-nested", nested.get("hard"));
nested = (Map) accessToken.getOtherClaims().get("home");
assertThat((List<String>) nested.get("phone"), hasItems("617-777-6666"));
departments = (List<String>) idToken.getOtherClaims().get("department");
assertEquals(2, departments.size());
assertTrue(departments.contains("finance") && departments.contains("development"));
assertTrue(accessToken.getRealmAccess().getRoles().contains("hardcoded"));
assertTrue(accessToken.getRealmAccess().getRoles().contains("realm-user"));
Assert.assertFalse(accessToken.getResourceAccess("test-app").getRoles().contains("customer-user"));
assertTrue(accessToken.getResourceAccess("app").getRoles().contains("hardcoded"));
}
// undo mappers
{
ClientResource app = findClientByClientId(adminClient.realm("test"), "test-app");
ClientRepresentation clientRepresentation = app.toRepresentation();
for (ProtocolMapperRepresentation model : clientRepresentation.getProtocolMappers()) {
if (model.getName().equals("address")
|| model.getName().equals("hard")
|| model.getName().equals("hard-nested")
|| model.getName().equals("custom phone")
|| model.getName().equals("departments")
|| model.getName().equals("firstDepartment")
|| model.getName().equals("nested phone")
|| model.getName().equals("rename-app-role")
|| model.getName().equals("hard-realm")
|| model.getName().equals("hard-app")
) {
app.getProtocolMappers().delete(model.getId());
}
}
}
events.clear();
{
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password");
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
assertNull(idToken.getAddress());
assertNull(idToken.getOtherClaims().get("home_phone"));
assertNull(idToken.getOtherClaims().get("hard"));
assertNull(idToken.getOtherClaims().get("nested"));
assertNull(idToken.getOtherClaims().get("department"));
}
events.clear();
}
@Test
public void testUserRoleToAttributeMappers() throws Exception {
// Add mapper for realm roles
ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true);
ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper("test-app", null, "Client roles mapper", "roles-custom.test-app", true, true);
ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), "test-app").getProtocolMappers();
protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper));
// Login user
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password");
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
// Verify attribute is filled
Map<String, Object> roleMappings = (Map<String, Object>)idToken.getOtherClaims().get("roles-custom");
Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", "test-app"));
String realmRoleMappings = (String) roleMappings.get("realm");
String testAppMappings = (String) roleMappings.get("test-app");
assertRolesString(realmRoleMappings,
"pref.user", // from direct assignment in user definition
"pref.offline_access" // from direct assignment in user definition
);
assertRolesString(testAppMappings,
"customer-user" // from direct assignment in user definition
);
// Revert
deleteMappers(protocolMappers);
}
/**
* KEYCLOAK-4205
* @throws Exception
*/
@Test
public void testUserRoleToAttributeMappersWithMultiValuedRoles() throws Exception {
// Add mapper for realm roles
ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true, true);
ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper("test-app", null, "Client roles mapper", "roles-custom.test-app", true, true, true);
ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), "test-app").getProtocolMappers();
protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper));
// Login user
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password");
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
// Verify attribute is filled
Map<String, Object> roleMappings = (Map<String, Object>)idToken.getOtherClaims().get("roles-custom");
Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", "test-app"));
Assert.assertThat(roleMappings.get("realm"), CoreMatchers.instanceOf(List.class));
Assert.assertThat(roleMappings.get("test-app"), CoreMatchers.instanceOf(List.class));
List<String> realmRoleMappings = (List<String>) roleMappings.get("realm");
List<String> testAppMappings = (List<String>) roleMappings.get("test-app");
assertRoles(realmRoleMappings,
"pref.user", // from direct assignment in user definition
"pref.offline_access" // from direct assignment in user definition
);
assertRoles(testAppMappings,
"customer-user" // from direct assignment in user definition
);
// Revert
deleteMappers(protocolMappers);
}
@Test
public void testUserGroupRoleToAttributeMappers() throws Exception {
// Add mapper for realm roles
String clientId = "test-app";
ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true);
ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper(clientId, "ta.", "Client roles mapper", "roles-custom.test-app", true, true);
ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), clientId).getProtocolMappers();
protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper));
// Login user
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "rich.roles@redhat.com", "password");
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
// Verify attribute is filled
Map<String, Object> roleMappings = (Map<String, Object>)idToken.getOtherClaims().get("roles-custom");
Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", clientId));
String realmRoleMappings = (String) roleMappings.get("realm");
String testAppMappings = (String) roleMappings.get(clientId);
assertRolesString(realmRoleMappings,
"pref.admin", // from direct assignment to /roleRichGroup/level2group
"pref.user", // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup
"pref.customer-user-premium", // from client role customer-admin-composite-role - realm role for test-app
"pref.realm-composite-role", // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup
"pref.sample-realm-role" // from realm role realm-composite-role
);
assertRolesString(testAppMappings,
"ta.customer-user", // from direct assignment to /roleRichGroup/level2group
"ta.customer-admin-composite-role", // from direct assignment to /roleRichGroup/level2group
"ta.customer-admin", // from client role customer-admin-composite-role - client role for test-app
"ta.sample-client-role" // from realm role realm-composite-role - client role for test-app
);
// Revert
deleteMappers(protocolMappers);
}
@Test
public void testUserGroupRoleToAttributeMappersNotScopedOtherApp() throws Exception {
String clientId = "test-app-authz";
ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true);
ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper(clientId, null, "Client roles mapper", "roles-custom." + clientId, true, true);
ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), clientId).getProtocolMappers();
protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper));
// Login user
ClientManager.realm(adminClient.realm("test")).clientId(clientId).directAccessGrant(true);
oauth.clientId(clientId);
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "rich.roles@redhat.com", "password");
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
// Verify attribute is filled
Map<String, Object> roleMappings = (Map<String, Object>)idToken.getOtherClaims().get("roles-custom");
Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", clientId));
String realmRoleMappings = (String) roleMappings.get("realm");
String testAppAuthzMappings = (String) roleMappings.get(clientId);
assertRolesString(realmRoleMappings,
"pref.admin", // from direct assignment to /roleRichGroup/level2group
"pref.user", // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup
"pref.customer-user-premium", // from client role customer-admin-composite-role - realm role for test-app
"pref.realm-composite-role", // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup
"pref.sample-realm-role" // from realm role realm-composite-role
);
assertRolesString(testAppAuthzMappings); // There is no client role defined for test-app-authz
// Revert
deleteMappers(protocolMappers);
}
@Test
public void testUserGroupRoleToAttributeMappersScoped() throws Exception {
String clientId = "test-app-scope";
ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true);
ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper(clientId, null, "Client roles mapper", "roles-custom.test-app-scope", true, true);
ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), clientId).getProtocolMappers();
protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper));
// Login user
ClientManager.realm(adminClient.realm("test")).clientId(clientId).directAccessGrant(true);
oauth.clientId(clientId);
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "rich.roles@redhat.com", "password");
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
// Verify attribute is filled
Map<String, Object> roleMappings = (Map<String, Object>)idToken.getOtherClaims().get("roles-custom");
Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", clientId));
String realmRoleMappings = (String) roleMappings.get("realm");
String testAppScopeMappings = (String) roleMappings.get(clientId);
assertRolesString(realmRoleMappings,
"pref.admin", // from direct assignment to /roleRichGroup/level2group
"pref.user" // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup
);
assertRolesString(testAppScopeMappings,
"test-app-allowed-by-scope" // from direct assignment to roleRichUser, present as scope allows it
);
// Revert
deleteMappers(protocolMappers);
}
@Test
public void testUserGroupRoleToAttributeMappersScopedClientNotSet() throws Exception {
String clientId = "test-app-scope";
ProtocolMapperRepresentation realmMapper = ProtocolMapperUtil.createUserRealmRoleMappingMapper("pref.", "Realm roles mapper", "roles-custom.realm", true, true);
ProtocolMapperRepresentation clientMapper = ProtocolMapperUtil.createUserClientRoleMappingMapper(null, null, "Client roles mapper", "roles-custom.test-app-scope", true, true);
ProtocolMappersResource protocolMappers = ApiUtil.findClientResourceByClientId(adminClient.realm("test"), clientId).getProtocolMappers();
protocolMappers.createMapper(Arrays.asList(realmMapper, clientMapper));
// Login user
ClientManager.realm(adminClient.realm("test")).clientId(clientId).directAccessGrant(true);
oauth.clientId(clientId);
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "rich.roles@redhat.com", "password");
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
// Verify attribute is filled
Map<String, Object> roleMappings = (Map<String, Object>)idToken.getOtherClaims().get("roles-custom");
Assert.assertThat(roleMappings.keySet(), containsInAnyOrder("realm", clientId));
String realmRoleMappings = (String) roleMappings.get("realm");
String testAppScopeMappings = (String) roleMappings.get(clientId);
assertRolesString(realmRoleMappings,
"pref.admin", // from direct assignment to /roleRichGroup/level2group
"pref.user" // from parent group of /roleRichGroup/level2group, i.e. from /roleRichGroup
);
assertRolesString(testAppScopeMappings,
"test-app-allowed-by-scope", // from direct assignment to roleRichUser, present as scope allows it
"customer-admin-composite-role" // from direct assignment to /roleRichGroup/level2group, present as scope allows it
);
// Revert
deleteMappers(protocolMappers);
}
private void assertRoles(List<String> actualRoleList, String ...expectedRoles){
Assert.assertNames(actualRoleList, expectedRoles);
}
private void assertRolesString(String actualRoleString, String...expectedRoles) {
Assert.assertThat(actualRoleString.matches("^\\[.*\\]$"), is(true));
String[] roles = actualRoleString.substring(1, actualRoleString.length() - 1).split(",\\s*");
if (expectedRoles == null || expectedRoles.length == 0) {
Assert.assertThat(roles, arrayContainingInAnyOrder(""));
} else {
Assert.assertThat(roles, arrayContainingInAnyOrder(expectedRoles));
}
}
private ProtocolMapperRepresentation makeMapper(String name, String mapperType, Map<String, String> config) {
ProtocolMapperRepresentation rep = new ProtocolMapperRepresentation();
rep.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
rep.setName(name);
rep.setProtocolMapper(mapperType);
rep.setConfig(config);
rep.setConsentRequired(true);
rep.setConsentText("Test Consent Text");
return rep;
}
}