Allow mapping claims to user attributes when exchanging tokens

Closes #8833
This commit is contained in:
Pedro Igor 2023-02-06 20:57:54 -03:00 committed by Marek Posolda
parent e38b7adf92
commit 7b58783255
4 changed files with 201 additions and 4 deletions

View file

@ -186,6 +186,25 @@ public class BrokeredIdentityContext {
}
}
public Map<String, List<String>> getAttributes() {
Map<String, List<String>> result = new HashMap<>();
for (Map.Entry<String, Object> entry : this.contextData.entrySet()) {
if (entry.getKey().startsWith(Constants.USER_ATTRIBUTES_PREFIX)) {
String attrName = entry.getKey().substring(Constants.USER_ATTRIBUTES_PREFIX.length());
List<String> asList = (List<String>) getContextData().get(Constants.USER_ATTRIBUTES_PREFIX + attrName);
if (asList.isEmpty()) {
continue;
}
result.put(attrName, asList);
}
}
return result;
}
public String getFirstName() {
return firstName;
}

View file

@ -24,6 +24,8 @@ import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
import org.keycloak.broker.oidc.OIDCIdentityProvider;
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.util.JsonSerialization;
@ -80,12 +82,24 @@ public abstract class AbstractClaimMapper extends AbstractIdentityProviderMapper
}
{ // search ID Token
JsonWebToken token = (JsonWebToken)context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ID_TOKEN);
if (token != null) {
Object value = getClaimValue(token, claim);
if (value != null) return value;
Object rawIdToken = context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ID_TOKEN);
JsonWebToken idToken;
if (rawIdToken instanceof String) {
try {
idToken = new JWSInput(rawIdToken.toString()).readJsonContent(JsonWebToken.class);
} catch (JWSInputException e) {
return null;
}
} else if (rawIdToken instanceof JsonWebToken) {
idToken = (JsonWebToken) rawIdToken;
} else {
return null;
}
Object value = getClaimValue(idToken, claim);
if (value != null)
return value;
}
{
// Search the OIDC UserInfo claim set (if any)

View file

@ -74,6 +74,7 @@ import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_CLIENT;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
@ -607,6 +608,14 @@ public class DefaultTokenExchangeProvider implements TokenExchangeProvider {
IdentityProviderMapperSyncModeDelegate.delegateUpdateBrokeredUser(session, realm, user, mapper, context, target);
}
}
// make sure user attributes are updated based on attributes set to the context
for (Map.Entry<String, List<String>> attr : context.getAttributes().entrySet()) {
if (!UserModel.USERNAME.equalsIgnoreCase(attr.getKey())) {
user.setAttribute(attr.getKey(), attr.getValue());
}
}
return user;
}

View file

@ -0,0 +1,155 @@
/*
* Copyright 2023 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.broker;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS;
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createHardcodedClaim;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.IdentityProviderResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.broker.oidc.mappers.UserAttributeMapper;
import org.keycloak.common.Profile;
import org.keycloak.models.ClientModel;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderMapperSyncMode;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.arquillian.annotation.EnableFeatures;
import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.util.BasicAuthHelper;
import com.google.common.collect.ImmutableMap;
public final class KcOidcBrokerTokenExchangeTest extends AbstractInitializedBaseBrokerTest {
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return KcOidcBrokerConfiguration.INSTANCE;
}
@Test
@EnableFeatures({ @EnableFeature(Profile.Feature.TOKEN_EXCHANGE), @EnableFeature(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ) })
public void testExternalInternalTokenExchange() throws Exception {
RealmResource providerRealm = realmsResouce().realm(bc.providerRealmName());
ClientsResource clients = providerRealm.clients();
ClientRepresentation brokerApp = clients.findByClientId("brokerapp").get(0);
brokerApp.setDirectAccessGrantsEnabled(true);
ClientResource brokerAppResource = providerRealm.clients().get(brokerApp.getId());
brokerAppResource.update(brokerApp);
brokerAppResource.getProtocolMappers().createMapper(createHardcodedClaim("hard-coded", "hard-coded", "hard-coded", "String", true, true)).close();
IdentityProviderMapperRepresentation hardCodedSessionNoteMapper = new IdentityProviderMapperRepresentation();
hardCodedSessionNoteMapper.setName("hard-coded");
hardCodedSessionNoteMapper.setIdentityProviderAlias(bc.getIDPAlias());
hardCodedSessionNoteMapper.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID);
hardCodedSessionNoteMapper.setConfig(ImmutableMap.<String, String>builder()
.put(IdentityProviderMapperModel.SYNC_MODE, IdentityProviderMapperSyncMode.INHERIT.toString())
.put(UserAttributeMapper.USER_ATTRIBUTE, "mapped-from-claim")
.put(UserAttributeMapper.CLAIM, "hard-coded")
.build());
RealmResource consumerRealm = realmsResouce().realm(bc.consumerRealmName());
IdentityProviderResource identityProviderResource = consumerRealm.identityProviders().get(bc.getIDPAlias());
identityProviderResource.addMapper(hardCodedSessionNoteMapper).close();
OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest(bc.providerRealmName(), bc.getUserLogin(), bc.getUserPassword(), null, brokerApp.getClientId(), brokerApp.getSecret());
assertThat(tokenResponse.getIdToken(), notNullValue());
testingClient.server().run(KcOidcBrokerTokenExchangeTest::setupRealm);
ClientRepresentation client = consumerRealm.clients().findByClientId("test-app").get(0);
Client httpClient = AdminClientUtil.createResteasyClient();
try {
WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
.path("/realms")
.path(bc.consumerRealmName())
.path("protocol/openid-connect/token");
// test user info validation.
try (Response response = exchangeUrl.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(
client.getClientId(), client.getSecret()))
.post(Entity.form(
new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.SUBJECT_TOKEN, tokenResponse.getIdToken())
.param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ID_TOKEN_TYPE)
.param(OAuth2Constants.SUBJECT_ISSUER, bc.getIDPAlias())
.param(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
))) {
assertThat(response.getStatus(), equalTo(200));
UserRepresentation user = consumerRealm.users().search(bc.getUserLogin()).get(0);
assertThat(user.getAttributes().get("mapped-from-claim").get(0), equalTo("hard-coded"));
}
} finally {
httpClient.close();
}
}
private static void setupRealm(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName(BrokerTestConstants.REALM_CONS_NAME);
IdentityProviderModel idp = realm.getIdentityProviderByAlias(IDP_OIDC_ALIAS);
org.junit.Assert.assertNotNull(idp);
ClientModel client = realm.addClient("test-app");
client.setClientId("test-app");
client.setPublicClient(false);
client.setDirectAccessGrantsEnabled(true);
client.setEnabled(true);
client.setSecret("secret");
client.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
client.setFullScopeAllowed(false);
AdminPermissionManagement management = AdminPermissions.management(session, realm);
management.idps().setPermissionsEnabled(idp, true);
ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation();
clientRep.setName("toIdp");
clientRep.addClient(client.getId());
ResourceServer server = management.realmResourceServer();
Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(server, clientRep);
management.idps().exchangeToPermission(idp).addAssociatedPolicy(clientPolicy);
}
}