KEYCLOAK-18146 Search for clients by client attribute when doing saml artifact resolution

This commit is contained in:
Michal Hajas 2021-05-26 15:29:15 +02:00 committed by Hynek Mlnařík
parent 2cb59e2503
commit 4dcb69596b
26 changed files with 364 additions and 52 deletions

View file

@ -0,0 +1,84 @@
/*
* Copyright 2020 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.connections.jpa.updater.liquibase.custom;
import liquibase.exception.CustomChangeException;
import liquibase.statement.core.InsertStatement;
import liquibase.structure.core.Table;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.HashMap;
import java.util.Map;
import static org.keycloak.protocol.saml.util.ArtifactBindingUtils.computeArtifactBindingIdentifierString;
public class JpaUpdate14_0_0_MigrateSamlArtifactAttribute extends CustomKeycloakTask {
private static final String SAML_ARTIFACT_BINDING_IDENTIFIER = "saml.artifact.binding.identifier";
private final Map<String, String> clientIds = new HashMap<>();
@Override
protected void generateStatementsImpl() throws CustomChangeException {
extractClientsData("SELECT C.ID, C.CLIENT_ID FROM " + getTableName("CLIENT") + " C " +
"LEFT JOIN " + getTableName("CLIENT_ATTRIBUTES") + " CA " +
"ON C.ID = CA.CLIENT_ID AND CA.NAME='" + SAML_ARTIFACT_BINDING_IDENTIFIER + "' " +
"WHERE C.PROTOCOL='saml' AND CA.NAME IS NULL");
for (Map.Entry<String, String> clientPair : clientIds.entrySet()) {
String id = clientPair.getKey();
String clientId = clientPair.getValue();
String samlIdentifier = computeArtifactBindingIdentifierString(clientId);
statements.add(
new InsertStatement(null, null, database.correctObjectName("CLIENT_ATTRIBUTES", Table.class))
.addColumnValue("CLIENT_ID", id)
.addColumnValue("NAME", SAML_ARTIFACT_BINDING_IDENTIFIER)
.addColumnValue("VALUE", samlIdentifier)
);
}
}
private void extractClientsData(String sql) throws CustomChangeException {
try (PreparedStatement statement = jdbcConnection.prepareStatement(sql);
ResultSet rs = statement.executeQuery()) {
while (rs.next()) {
String id = rs.getString(1);
String clientId = rs.getString(2);
if (id == null || id.trim().isEmpty()
|| clientId == null || clientId.trim().isEmpty()) {
continue;
}
clientIds.put(id, clientId);
}
} catch (Exception e) {
throw new CustomChangeException(getTaskId() + ": Exception when extracting data from previous version", e);
}
}
@Override
protected String getTaskId() {
return "Migrate Saml attributes (14.0.0)";
}
}

View file

@ -23,6 +23,7 @@ import org.keycloak.models.ClientProvider;
import org.keycloak.models.ClientProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.protocol.saml.SamlConfigAttributes;
import javax.persistence.EntityManager;
import java.util.Arrays;
@ -38,7 +39,8 @@ public class JpaClientProviderFactory implements ClientProviderFactory {
private Set<String> clientSearchableAttributes = null;
private static final List<String> REQUIRED_SEARCHABLE_ATTRIBUTES = Arrays.asList(
"saml_idp_initiated_sso_url_name"
"saml_idp_initiated_sso_url_name",
SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER
);
@Override

View file

@ -71,4 +71,8 @@
</createIndex>
</changeSet>
<changeSet author="keycloak" id="KEYCLOAK-18146-add-saml-art-binding-identifier">
<customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.JpaUpdate14_0_0_MigrateSamlArtifactAttribute"/>
</changeSet>
</databaseChangeLog>

View file

@ -141,6 +141,8 @@ import org.keycloak.storage.federated.UserFederatedStorageProvider;
import org.keycloak.util.JsonSerialization;
import org.keycloak.validation.ValidationUtil;
import static org.keycloak.protocol.saml.util.ArtifactBindingUtils.computeArtifactBindingIdentifierString;
public class RepresentationToModel {
private static Logger logger = Logger.getLogger(RepresentationToModel.class);
@ -1407,6 +1409,11 @@ public class RepresentationToModel {
}
}
if ("saml".equals(resourceRep.getProtocol())
&& (resourceRep.getAttributes() == null
|| !resourceRep.getAttributes().containsKey("saml.artifact.binding.identifier"))) {
client.setAttribute("saml.artifact.binding.identifier", computeArtifactBindingIdentifierString(resourceRep.getClientId()));
}
if (resourceRep.getAuthenticationFlowBindingOverrides() != null) {
for (Map.Entry<String, String> entry : resourceRep.getAuthenticationFlowBindingOverrides().entrySet()) {
@ -1559,6 +1566,12 @@ public class RepresentationToModel {
}
}
if ("saml".equals(rep.getProtocol())
&& (rep.getAttributes() == null
|| !rep.getAttributes().containsKey("saml.artifact.binding.identifier"))) {
resource.setAttribute("saml.artifact.binding.identifier", computeArtifactBindingIdentifierString(rep.getClientId()));
}
if (rep.getAuthenticationFlowBindingOverrides() != null) {
for (Map.Entry<String, String> entry : rep.getAuthenticationFlowBindingOverrides().entrySet()) {
if (entry.getValue() == null || entry.getValue().trim().equals("")) {

View file

@ -2,10 +2,9 @@ package org.keycloak.protocol.saml;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.Provider;
import java.util.stream.Stream;
/**
* Provides a way to create and resolve artifacts for SAML Artifact binding
@ -15,12 +14,12 @@ public interface ArtifactResolver extends Provider {
/**
* Returns client model that issued artifact
*
* @param session KeycloakSession for searching for client corresponding client
* @param artifact the artifact
* @param clients stream of clients, the stream will be searched for a client that issued the artifact
* @return the client model that issued the artifact
* @throws ArtifactResolverProcessingException When an error occurs during client search
*/
ClientModel selectSourceClient(String artifact, Stream<ClientModel> clients) throws ArtifactResolverProcessingException;
ClientModel selectSourceClient(KeycloakSession session, String artifact) throws ArtifactResolverProcessingException;
/**
* Creates and stores an artifact

View file

@ -0,0 +1,51 @@
package org.keycloak.protocol.saml.util;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
public class ArtifactBindingUtils {
public static String artifactToResolverProviderId(String artifact) {
return byteArrayToResolverProviderId(Base64.getDecoder().decode(artifact));
}
public static String byteArrayToResolverProviderId(byte[] ar) {
return String.format("%02X%02X", ar[0], ar[1]);
}
/**
* Computes identifier from the given String, for example, from entityId
*
* @param identifierFrom String that will be turned into an identifier
* @return Base64 of SHA-1 hash of the identifierFrom
*/
public static String computeArtifactBindingIdentifierString(String identifierFrom) {
return Base64.getEncoder().encodeToString(computeArtifactBindingIdentifier(identifierFrom));
}
/**
* Turns byte representation of the identifier into readable String
*
* @param identifier byte representation of the identifier
* @return Base64 of the identifier
*/
public static String getArtifactBindingIdentifierString(byte[] identifier) {
return Base64.getEncoder().encodeToString(identifier);
}
/**
* Computes 20 bytes long byte identifier of the given string, for example, from entityId
*
* @param identifierFrom String that will be turned into an identifier
* @return SHA-1 hash of the given identifierFrom
*/
public static byte[] computeArtifactBindingIdentifier(String identifierFrom) {
try {
MessageDigest sha1Digester = MessageDigest.getInstance("SHA-1");
return sha1Digester.digest(identifierFrom.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("JVM does not support required cryptography algorithms: SHA-1/SHA1PRNG.", e);
}
}
}

View file

@ -1,22 +1,22 @@
package org.keycloak.protocol.saml;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import org.jboss.logging.Logger;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.saml.util.ArtifactBindingUtils;
import org.keycloak.saml.common.constants.GeneralConstants;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import java.util.stream.Stream;
import java.util.Collections;
import static org.keycloak.protocol.saml.DefaultSamlArtifactResolverFactory.TYPE_CODE;
import static org.keycloak.protocol.saml.SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER;
/**
* ArtifactResolver for artifact-04 format.
@ -43,18 +43,13 @@ public class DefaultSamlArtifactResolver implements ArtifactResolver {
}
@Override
public ClientModel selectSourceClient(String artifact, Stream<ClientModel> clients) throws ArtifactResolverProcessingException {
try {
public ClientModel selectSourceClient(KeycloakSession session, String artifact) throws ArtifactResolverProcessingException {
byte[] source = extractSourceFromArtifact(artifact);
String identifier = ArtifactBindingUtils.getArtifactBindingIdentifierString(source);
MessageDigest sha1Digester = MessageDigest.getInstance("SHA-1");
return clients.filter(clientModel -> Arrays.equals(source,
sha1Digester.digest(clientModel.getClientId().getBytes(Charsets.UTF_8))))
.findFirst()
.orElseThrow(() -> new ArtifactResolverProcessingException("No client matching the artifact source found"));
} catch (NoSuchAlgorithmException e) {
throw new ArtifactResolverProcessingException(e);
}
return session.clients().searchClientsByAttributes(session.getContext().getRealm(),
Collections.singletonMap(SAML_ARTIFACT_BINDING_IDENTIFIER, identifier), 0, 1)
.findFirst().orElseThrow(() -> new ArtifactResolverProcessingException("No client matching the artifact source found"));
}
@Override
@ -109,8 +104,7 @@ public class DefaultSamlArtifactResolver implements ArtifactResolver {
SecureRandom handleGenerator = SecureRandom.getInstance("SHA1PRNG");
byte[] trimmedIndex = new byte[2];
MessageDigest sha1Digester = MessageDigest.getInstance("SHA-1");
byte[] source = sha1Digester.digest(entityId.getBytes(Charsets.UTF_8));
byte[] source = ArtifactBindingUtils.computeArtifactBindingIdentifier(entityId);
byte[] assertionHandle = new byte[20];
handleGenerator.nextBytes(assertionHandle);

View file

@ -20,6 +20,7 @@ package org.keycloak.protocol.saml;
import org.jboss.logging.Logger;
import org.keycloak.models.ClientConfigResolver;
import org.keycloak.models.ClientModel;
import org.keycloak.protocol.saml.util.ArtifactBindingUtils;
import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer;
@ -258,4 +259,12 @@ public class SamlClient extends ClientConfigResolver {
return -1;
}
}
public void setArtifactBindingIdentifierFrom(String identifierFrom) {
client.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER, ArtifactBindingUtils.computeArtifactBindingIdentifierString(identifierFrom));
}
public String getArtifactBindingIdentifier() {
return client.getAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER);
}
}

View file

@ -43,4 +43,5 @@ public interface SamlConfigAttributes {
String SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE = "saml.encryption." + CertificateInfoHelper.X509CERTIFICATE;
String SAML_ENCRYPTION_PRIVATE_KEY_ATTRIBUTE = "saml.encryption." + CertificateInfoHelper.PRIVATE_KEY;
String SAML_ASSERTION_LIFESPAN = "saml.assertion.lifespan";
String SAML_ARTIFACT_BINDING_IDENTIFIER = "saml.artifact.binding.identifier";
}

View file

@ -175,6 +175,8 @@ public class SamlProtocolFactory extends AbstractLoginProtocolFactory {
if (clientRep.isFrontchannelLogout() == null) {
newClient.setFrontchannelLogout(true);
}
client.setArtifactBindingIdentifierFrom(clientRep.getClientId());
}
}

View file

@ -71,7 +71,7 @@ import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor;
import org.keycloak.protocol.saml.profile.ecp.SamlEcpProfileService;
import org.keycloak.protocol.saml.profile.util.Soap;
import org.keycloak.protocol.util.ArtifactBindingUtils;
import org.keycloak.protocol.saml.util.ArtifactBindingUtils;
import org.keycloak.rotation.HardcodedKeyLocator;
import org.keycloak.rotation.KeyLocator;
import org.keycloak.saml.BaseSAML2BindingBuilder;
@ -341,7 +341,7 @@ public class SamlService extends AuthorizationEndpointBase {
//Find client
ClientModel client;
try {
client = getArtifactResolver(artifact).selectSourceClient(artifact, realm.getClientsStream());
client = getArtifactResolver(artifact).selectSourceClient(session, artifact);
Response error = checkClientValidity(client);
if (error != null) {

View file

@ -1,15 +0,0 @@
package org.keycloak.protocol.util;
import org.keycloak.protocol.saml.DefaultSamlArtifactResolverFactory;
import java.util.Base64;
public class ArtifactBindingUtils {
public static String artifactToResolverProviderId(String artifact) {
return byteArrayToResolverProviderId(Base64.getDecoder().decode(artifact));
}
public static String byteArrayToResolverProviderId(byte[] ar) {
return String.format("%02X%02X", ar[0], ar[1]);
}
}

View file

@ -35,6 +35,9 @@ import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
import org.keycloak.protocol.saml.SamlClient;
import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.representations.adapters.config.BaseRealmConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.idm.ClientRepresentation;
@ -191,7 +194,8 @@ public class ClientManager {
}
}
public void clientIdChanged(ClientModel client, String newClientId) {
public void clientIdChanged(ClientModel client, ClientRepresentation newClientRepresentation) {
String newClientId = newClientRepresentation.getClientId();
logger.debugf("Updating clientId from '%s' to '%s'", client.getClientId(), newClientId);
UserModel serviceAccountUser = realmManager.getSession().users().getServiceAccount(client);
@ -199,6 +203,13 @@ public class ClientManager {
String username = ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + newClientId;
serviceAccountUser.setUsername(username);
}
if (SamlProtocol.LOGIN_PROTOCOL.equals(client.getProtocol())) {
SamlClient samlClient = new SamlClient(client);
samlClient.setArtifactBindingIdentifierFrom(newClientId);
newClientRepresentation.getAttributes().put(SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER, samlClient.getArtifactBindingIdentifier());
}
}
@JsonPropertyOrder({"realm", "realm-public-key", "bearer-only", "auth-server-url", "ssl-required",

View file

@ -667,7 +667,7 @@ public class ClientResource {
}
if (rep.getClientId() != null && !rep.getClientId().equals(client.getClientId())) {
new ClientManager(new RealmManager(session)).clientIdChanged(client, rep.getClientId());
new ClientManager(new RealmManager(session)).clientIdChanged(client, rep);
}
if (rep.isFullScopeAllowed() != null && rep.isFullScopeAllowed() != client.isFullScopeAllowed()) {

View file

@ -2,6 +2,7 @@ package org.keycloak.testsuite.authentication;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.saml.ArtifactResolver;
import java.io.ByteArrayInputStream;
@ -10,7 +11,6 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.stream.Stream;
import static org.keycloak.testsuite.authentication.CustomTestingSamlArtifactResolverFactory.TYPE_CODE;
@ -23,7 +23,7 @@ public class CustomTestingSamlArtifactResolver implements ArtifactResolver {
public static List<String> list = new ArrayList<>();
@Override
public ClientModel selectSourceClient(String artifact, Stream<ClientModel> clients) {
public ClientModel selectSourceClient(KeycloakSession session, String artifact) {
return null;
}

View file

@ -5,7 +5,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.protocol.saml.ArtifactResolver;
import org.keycloak.protocol.saml.ArtifactResolverFactory;
import org.keycloak.protocol.util.ArtifactBindingUtils;
import org.keycloak.protocol.saml.util.ArtifactBindingUtils;
/**
* This ArtifactResolver should be used only for testing purposes.

View file

@ -25,7 +25,9 @@ import org.keycloak.client.registration.Auth;
import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.HttpErrorException;
import org.keycloak.events.Errors;
import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
import org.keycloak.protocol.saml.util.ArtifactBindingUtils;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.idm.ClientRepresentation;
@ -85,6 +87,7 @@ public class SAMLClientRegistrationTest extends AbstractClientRegistrationTest {
));
assertThat(response.getAttributes().get("saml_single_logout_service_url_redirect"), is("https://LoadBalancer-9.siroe.com:3443/federation/SPSloRedirect/metaAlias/sp"));
assertThat(response.getAttributes().get(SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER), is(ArtifactBindingUtils.computeArtifactBindingIdentifierString("loadbalancer-9.siroe.com")));
Assert.assertNotNull(response.getProtocolMappers());
Assert.assertEquals(1,response.getProtocolMappers().size());

View file

@ -45,6 +45,8 @@ import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.protocol.saml.util.ArtifactBindingUtils;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
@ -78,6 +80,7 @@ import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -86,6 +89,7 @@ import java.util.stream.Collectors;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@ -125,7 +129,7 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
protected void testMigratedMigrationData(boolean supportsAuthzService) {
assertNames(migrationRealm.roles().list(), "offline_access", "uma_authorization", "default-roles-migration", "migration-test-realm-role");
List<String> expectedClientIds = new ArrayList<>(Arrays.asList("account", "account-console", "admin-cli", "broker", "migration-test-client", "realm-management", "security-admin-console"));
List<String> expectedClientIds = new ArrayList<>(Arrays.asList("account", "account-console", "admin-cli", "broker", "migration-test-client", "migration-saml-client", "realm-management", "security-admin-console"));
if (supportsAuthzService) {
expectedClientIds.add("authz-servlet");
@ -304,6 +308,10 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
}
}
protected void testMigrationTo14_0_0() {
testSamlAttributes(migrationRealm);
}
protected void testDeleteAccount(RealmResource realm) {
ClientRepresentation accountClient = realm.clients().findByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).get(0);
ClientResource accountResource = realm.clients().get(accountClient.getId());
@ -924,6 +932,7 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
protected void testMigrationTo12_x(boolean testRealmAttributesMigration) {
testMigrationTo12_0_0();
testMigrationTo13_0_0(testRealmAttributesMigration);
testMigrationTo14_0_0();
}
protected void testMigrationTo7_x(boolean supportedAuthzServices) {
@ -965,6 +974,17 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
assertThat(migrationRealm2.toRepresentation().getDefaultRole().getName(), equalTo("default-roles-migration2-1"));
}
protected void testSamlAttributes(RealmResource realm) {
log.info("Testing SAML ARTIFACT BINDING IDENTIFIER");
realm.clients().findAll().stream()
.filter(clientRepresentation -> Objects.equals("saml", clientRepresentation.getProtocol()))
.forEach(clientRepresentation -> {
String clientId = clientRepresentation.getClientId();
assertThat(clientRepresentation.getAttributes(), hasEntry(SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER, ArtifactBindingUtils.computeArtifactBindingIdentifierString(clientId)));
});
}
protected void testRealmAttributesMigration() {
log.info("testing realm attributes migration");
Map<String, String> realmAttributes = migrationRealm.toRepresentation().getAttributes();

View file

@ -19,6 +19,7 @@ import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.protocol.saml.SamlProtocolUtils;
import org.keycloak.protocol.saml.profile.util.Soap;
import org.keycloak.protocol.saml.util.ArtifactBindingUtils;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.saml.SAML2LogoutResponseBuilder;
import org.keycloak.saml.common.constants.GeneralConstants;
@ -984,4 +985,39 @@ public class ArtifactBindingTest extends AbstractSamlTest {
assertThat(spDescriptor.getSingleLogoutService().get(0).getLocation(), is(equalTo(new URI("http://url.artifact.test"))));
}
@Test
public void testArtifactBindingIdentifierChangedWhenClientIdChanged() throws IOException {
ClientRepresentation clientRepresentation = adminClient.realm(REALM_NAME)
.clients()
.findByClientId(SAML_CLIENT_ID_SALES_POST)
.get(0);
String oldIdentifier = clientRepresentation.getAttributes().get(SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER);
assertThat(oldIdentifier, notNullValue());
final String newClientId = "new_client_id";
try (ClientAttributeUpdater cau = ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
.setClientId(newClientId)
.update()
) {
clientRepresentation = adminClient.realm(REALM_NAME)
.clients()
.findByClientId(newClientId)
.get(0);
String identifier = clientRepresentation.getAttributes().get(SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER);
assertThat(identifier, not(equalTo(oldIdentifier)));
assertThat(identifier, equalTo(ArtifactBindingUtils.computeArtifactBindingIdentifierString(newClientId)));
}
clientRepresentation = adminClient.realm(REALM_NAME)
.clients()
.findByClientId(SAML_CLIENT_ID_SALES_POST)
.get(0);
assertThat(clientRepresentation.getAttributes().get(SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER), equalTo(oldIdentifier));
}
}

View file

@ -2225,6 +2225,21 @@
"useTemplateConfig" : false,
"useTemplateScope" : false,
"useTemplateMappers" : false
}, {
"clientId": "migration-saml-client",
"enabled": true,
"fullScopeAllowed": true,
"protocol": "saml",
"baseUrl": "http://localhost:8080/sales-post",
"redirectUris": [
"http://localhost:8080/sales-post/*"
],
"attributes": {
"saml_assertion_consumer_url_post": "http://localhost:8080/sales-post/saml",
"saml_single_logout_service_url_post": "http://localhost:8080/sales-post/saml",
"saml.authnstatement": "true",
"saml_idp_initiated_sso_url_name": "sales-post"
}
}, {
"id" : "e6856a02-8f24-48d3-bb06-fae5dddae83e",
"clientId" : "realm-management",

View file

@ -2602,6 +2602,21 @@
"useTemplateConfig" : false,
"useTemplateScope" : false,
"useTemplateMappers" : false
}, {
"clientId": "migration-saml-client",
"enabled": true,
"fullScopeAllowed": true,
"protocol": "saml",
"baseUrl": "http://localhost:8080/sales-post",
"redirectUris": [
"http://localhost:8080/sales-post/*"
],
"attributes": {
"saml_assertion_consumer_url_post": "http://localhost:8080/sales-post/saml",
"saml_single_logout_service_url_post": "http://localhost:8080/sales-post/saml",
"saml.authnstatement": "true",
"saml_idp_initiated_sso_url_name": "sales-post"
}
}, {
"id" : "c8204f6f-f8c2-4af8-9bac-c45c95b4673b",
"clientId" : "realm-management",

View file

@ -810,6 +810,21 @@
"useTemplateConfig" : false,
"useTemplateScope" : false,
"useTemplateMappers" : false
}, {
"clientId": "migration-saml-client",
"enabled": true,
"fullScopeAllowed": true,
"protocol": "saml",
"baseUrl": "http://localhost:8080/sales-post",
"redirectUris": [
"http://localhost:8080/sales-post/*"
],
"attributes": {
"saml_assertion_consumer_url_post": "http://localhost:8080/sales-post/saml",
"saml_single_logout_service_url_post": "http://localhost:8080/sales-post/saml",
"saml.authnstatement": "true",
"saml_idp_initiated_sso_url_name": "sales-post"
}
}, {
"id" : "9a37d2c5-6a36-4a2c-b837-f2ea846fb0d5",
"clientId" : "realm-management",

View file

@ -528,6 +528,21 @@
"nodeReRegistrationTimeout" : -1,
"defaultClientScopes" : [ "web-origins", "role_list", "profile", "roles", "email" ],
"optionalClientScopes" : [ "address", "phone", "offline_access" ]
}, {
"clientId": "migration-saml-client",
"enabled": true,
"fullScopeAllowed": true,
"protocol": "saml",
"baseUrl": "http://localhost:8080/sales-post",
"redirectUris": [
"http://localhost:8080/sales-post/*"
],
"attributes": {
"saml_assertion_consumer_url_post": "http://localhost:8080/sales-post/saml",
"saml_single_logout_service_url_post": "http://localhost:8080/sales-post/saml",
"saml.authnstatement": "true",
"saml_idp_initiated_sso_url_name": "sales-post"
}
}, {
"id" : "99a28a93-e2e3-4b1d-a377-8a30f6b4930a",
"clientId" : "realm-management",

View file

@ -554,6 +554,21 @@
"defaultClientScopes" : [ "web-origins", "role_list", "profile", "roles", "email" ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ]
}, {
"clientId": "migration-saml-client",
"enabled": true,
"fullScopeAllowed": true,
"protocol": "saml",
"baseUrl": "http://localhost:8080/sales-post",
"redirectUris": [
"http://localhost:8080/sales-post/*"
],
"attributes": {
"saml_assertion_consumer_url_post": "http://localhost:8080/sales-post/saml",
"saml_single_logout_service_url_post": "http://localhost:8080/sales-post/saml",
"saml.authnstatement": "true",
"saml_idp_initiated_sso_url_name": "sales-post"
}
},{
"id" : "cb1a7042-228c-4f8e-b0c9-654f1855d1b8",
"clientId" : "realm-management",
"name" : "${client_realm-management}",

View file

@ -4,6 +4,8 @@ import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.protocol.saml.util.ArtifactBindingUtils;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.testsuite.console.AbstractConsoleTest;
@ -87,6 +89,7 @@ public abstract class AbstractClientTest extends AbstractConsoleTest {
attributes.put(SAML_SIGNATURE_ALGORITHM, "RSA_SHA256");
attributes.put(SAML_FORCE_NAME_ID_FORMAT, "false");
attributes.put(SAML_NAME_ID_FORMAT, "username");
attributes.put(SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER, ArtifactBindingUtils.computeArtifactBindingIdentifierString("saml"));
return attributes;
}

View file

@ -20,6 +20,8 @@ package org.keycloak.testsuite.console.clients;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Test;
import org.keycloak.common.Profile;
import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.protocol.saml.util.ArtifactBindingUtils;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.console.page.clients.settings.ClientSettings;
@ -29,6 +31,7 @@ import org.openqa.selenium.By;
import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.*;
import static org.keycloak.testsuite.auth.page.login.Login.OIDC;
@ -202,6 +205,23 @@ public class ClientSettingsTest extends AbstractClientTest {
assertClientSamlAttributes(getSAMLAttributes(), found.getAttributes());
}
@Test
public void updateSAML() {
createSAML();
final String newClientId = "new_client_id";
clientSettingsPage.form().setClientId(newClientId);
clientSettingsPage.form().save();
ClientRepresentation found = findClientByClientId(newClientId);
Map<String, String> samlAttributes = getSAMLAttributes();
samlAttributes.put(SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER, ArtifactBindingUtils.computeArtifactBindingIdentifierString(newClientId));
assertClientSamlAttributes(samlAttributes, found.getAttributes());
}
@Test
public void invalidSettings() {
clientsPage.table().createClient();