Allow mapping claims to user attributes when exchanging tokens
Closes #8833
This commit is contained in:
parent
e38b7adf92
commit
7b58783255
4 changed files with 201 additions and 4 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue