fix: PEM files distributed as part of SAML adapter configs for mod_auth_mellon export

Changing return type of ClientResource from String to Response to support different response types. Should not be breaking as this is just a class used internally by Keycloak integration tests.

Closes #34276

Co-authored-by: ccudennec-otto
Co-authored-by: radwa-otto
Co-authored-by: IngoStrauch2020

Signed-off-by: Jan-Hendrik Dolling <jan-hendrik.dolling@otto.de>
This commit is contained in:
Jan-Hendrik Dolling
2024-11-13 14:03:00 +01:00
committed by Marek Posolda
parent cfd187b0ff
commit 80bbb0be10
6 changed files with 120 additions and 26 deletions

View File

@@ -124,6 +124,13 @@ public class PemUtils {
.toString();
}
public static String addCertificateBeginEnd(String certificate) {
return new StringBuilder(BEGIN_CERT + "\n")
.append(certificate)
.append("\n" + END_CERT)
.toString();
}
public static String addRsaPrivateKeyBeginEnd(String privateKeyPem) {
return new StringBuilder(PemUtils.BEGIN_RSA_PRIVATE_KEY + "\n")
.append(privateKeyPem)

View File

@@ -31,6 +31,7 @@ import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.keycloak.representations.adapters.action.GlobalRequestResult;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation;
@@ -115,7 +116,7 @@ public interface ClientResource {
@GET
@Path("installation/providers/{providerId}")
String getInstallationProvider(@PathParam("providerId") String providerId);
Response getInstallationProvider(@PathParam("providerId") String providerId);
@Path("session-count")
@GET
@@ -217,4 +218,4 @@ public interface ClientResource {
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public void invalidateRotatedSecret();
}
}

View File

@@ -17,7 +17,12 @@
package org.keycloak.protocol.saml.installation;
import java.nio.charset.StandardCharsets;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.keycloak.Config;
import org.keycloak.common.util.PemUtils;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@@ -59,12 +64,12 @@ public class ModAuthMellonClientInstallation implements ClientInstallationProvid
if (samlClient.requiresClientSignature()) {
if (samlClient.getClientSigningPrivateKey() != null) {
zip.putNextEntry(new ZipEntry(clientDirName + "/client-private-key.pem"));
zip.write(samlClient.getClientSigningPrivateKey().getBytes());
zip.write(createClientSigningPrivateKeyRfc7468Representation(samlClient.getClientSigningPrivateKey()));
zip.closeEntry();
}
if (samlClient.getClientSigningCertificate() != null) {
zip.putNextEntry(new ZipEntry(clientDirName + "/client-cert.pem"));
zip.write(samlClient.getClientSigningCertificate().getBytes());
zip.write(createClientSigningCertificateRfc7468Representation(samlClient.getClientSigningCertificate()));
zip.closeEntry();
}
}
@@ -132,4 +137,22 @@ public class ModAuthMellonClientInstallation implements ClientInstallationProvid
public String getId() {
return "mod-auth-mellon";
}
private static byte[] createClientSigningPrivateKeyRfc7468Representation(String clientSigningPrivateKey) {
String resultAsString = PemUtils.addPrivateKeyBeginEnd(wrapAt64Chars(clientSigningPrivateKey));
return resultAsString.getBytes(StandardCharsets.US_ASCII);
}
private static byte[] createClientSigningCertificateRfc7468Representation(String clientSigningCertificate) {
String resultAsString = PemUtils.addCertificateBeginEnd(wrapAt64Chars(clientSigningCertificate));
return resultAsString.getBytes(StandardCharsets.US_ASCII);
}
private static String wrapAt64Chars(String text) {
return Pattern.compile(".{1,64}")
.matcher(text)
.results()
.map(MatchResult::group)
.collect(Collectors.joining("\n"));
}
}

View File

@@ -45,6 +45,7 @@ public class SamlUtils {
.map(ClientRepresentation::getId)
.map(res::get)
.map(clientResource -> clientResource.getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR))
.map(response -> response.readEntity(String.class))
.orElseThrow(() -> new RuntimeException("Missing descriptor"));
SAMLParser parser = SAMLParser.getInstance();

View File

@@ -552,9 +552,10 @@ public class PermissionsTest extends AbstractKeycloakTest {
realm.clients().get(foo.getId()).toRepresentation();
}
}, Resource.CLIENT, false);
invoke(new Invocation() {
public void invoke(RealmResource realm) {
realm.clients().get(foo.getId()).getInstallationProvider("nosuch");
invoke(new InvocationWithResponse() {
public void invoke(RealmResource realm, AtomicReference<Response> responseRef) {
Response response = realm.clients().get(foo.getId()).getInstallationProvider("nosuch");
responseRef.set(response);
}
}, Resource.CLIENT, false);
invoke(new Invocation() {

View File

@@ -17,13 +17,18 @@
package org.keycloak.testsuite.admin.client;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import jakarta.ws.rs.core.Response;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -44,9 +49,11 @@ import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import jakarta.ws.rs.NotFoundException;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.keycloak.common.Profile.Feature.AUTHORIZATION;
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot;
@@ -103,27 +110,31 @@ public class InstallationTest extends AbstractClientTest {
@Test
public void testOidcJBossXml() {
String xml = oidcClient.getInstallationProvider("keycloak-oidc-jboss-subsystem");
Response response = oidcClient.getInstallationProvider("keycloak-oidc-jboss-subsystem");
String xml = response.readEntity(String.class);
assertOidcInstallationConfig(xml);
assertThat(xml, containsString("<secure-deployment"));
}
@Test
public void testOidcJson() {
String json = oidcClient.getInstallationProvider("keycloak-oidc-keycloak-json");
Response response = oidcClient.getInstallationProvider("keycloak-oidc-keycloak-json");
String json = response.readEntity(String.class);
assertOidcInstallationConfig(json);
}
@Test
public void testOidcJBossCli() {
String cli = oidcClient.getInstallationProvider("keycloak-oidc-jboss-subsystem-cli");
Response response = oidcClient.getInstallationProvider("keycloak-oidc-jboss-subsystem-cli");
String cli = response.readEntity(String.class);
assertOidcInstallationConfig(cli);
assertThat(cli, containsString("/subsystem=keycloak/secure-deployment=\"WAR MODULE NAME.war\""));
}
@Test
public void testOidcBearerOnlyJson() {
String json = oidcBearerOnlyClient.getInstallationProvider("keycloak-oidc-keycloak-json");
Response response = oidcBearerOnlyClient.getInstallationProvider("keycloak-oidc-keycloak-json");
String json = response.readEntity(String.class);
assertOidcInstallationConfig(json);
assertThat(json, containsString("bearer-only"));
assertThat(json, not(containsString("public-client")));
@@ -136,7 +147,8 @@ public class InstallationTest extends AbstractClientTest {
// Generate audience client scope
String clientScopeId = testingClient.testing().generateAudienceClientScope("test", OIDC_NAME_BEARER_ONLY_NAME);
String json = oidcBearerOnlyClient.getInstallationProvider("keycloak-oidc-keycloak-json");
Response response = oidcBearerOnlyClient.getInstallationProvider("keycloak-oidc-keycloak-json");
String json = response.readEntity(String.class);
assertOidcInstallationConfig(json);
assertThat(json, containsString("bearer-only"));
assertThat(json, not(containsString("public-client")));
@@ -155,7 +167,8 @@ public class InstallationTest extends AbstractClientTest {
oidcBearerOnlyClientWithAuthzId = createOidcConfidentialClientWithAuthz(OIDC_NAME_BEARER_ONLY_WITH_AUTHZ_NAME);
oidcBearerOnlyClientWithAuthz = findClientResource(OIDC_NAME_BEARER_ONLY_WITH_AUTHZ_NAME);
String json = oidcBearerOnlyClientWithAuthz.getInstallationProvider("keycloak-oidc-keycloak-json");
Response response = oidcBearerOnlyClientWithAuthz.getInstallationProvider("keycloak-oidc-keycloak-json");
String json = response.readEntity(String.class);
assertOidcInstallationConfig(json);
assertThat(json, not(containsString("bearer-only")));
assertThat(json, not(containsString("public-client")));
@@ -172,14 +185,16 @@ public class InstallationTest extends AbstractClientTest {
assertThat(config, containsString(authServerUrl()));
}
@Test(expected = NotFoundException.class)
@Test
public void testSamlMetadataIdpDescriptor() {
samlClient.getInstallationProvider("saml-idp-descriptor");
Response response = samlClient.getInstallationProvider("saml-idp-descriptor");
assertEquals(404, response.getStatus());
}
@Test
public void testSamlAdapterXml() {
String xml = samlClient.getInstallationProvider("keycloak-saml");
Response response = samlClient.getInstallationProvider("keycloak-saml");
String xml = response.readEntity(String.class);
assertThat(xml, containsString("<keycloak-saml-adapter>"));
assertThat(xml, containsString("SPECIFY YOUR entityID!"));
assertThat(xml, not(containsString(KeyUtils.findActiveSigningKey(testRealmResource()).getCertificate())));
@@ -188,7 +203,8 @@ public class InstallationTest extends AbstractClientTest {
@Test
public void testSamlAdapterCli() {
String cli = samlClient.getInstallationProvider("keycloak-saml-subsystem-cli");
Response response = samlClient.getInstallationProvider("keycloak-saml-subsystem-cli");
String cli = response.readEntity(String.class);
assertThat(cli, containsString("/subsystem=keycloak-saml/secure-deployment=YOUR-WAR.war/"));
assertThat(cli, containsString("SPECIFY YOUR entityID!"));
assertThat(cli, not(containsString(KeyUtils.findActiveSigningKey(testRealmResource()).getCertificate())));
@@ -197,7 +213,8 @@ public class InstallationTest extends AbstractClientTest {
@Test
public void testSamlMetadataSpDescriptor() throws Exception {
String xml = samlClient.getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR);
Response response = samlClient.getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR);
String xml = response.readEntity(String.class);
Document doc = getDocumentFromXmlString(xml);
assertElements(doc, METADATA_NSURI.get(), "EntityDescriptor", null);
assertElements(doc, METADATA_NSURI.get(), "SPSSODescriptor", null);
@@ -206,7 +223,8 @@ public class InstallationTest extends AbstractClientTest {
@Test
public void testSamlJBossXml() {
String xml = samlClient.getInstallationProvider("keycloak-saml-subsystem");
Response response = samlClient.getInstallationProvider("keycloak-saml-subsystem");
String xml = response.readEntity(String.class);
assertThat(xml, containsString("<secure-deployment"));
assertThat(xml, containsString("SPECIFY YOUR entityID!"));
assertThat(xml, not(containsString(KeyUtils.findActiveSigningKey(testRealmResource()).getCertificate())));
@@ -220,7 +238,8 @@ public class InstallationTest extends AbstractClientTest {
assertThat(updater.getResource().toRepresentation().getAttributes().get(SamlConfigAttributes.SAML_FORCE_POST_BINDING), equalTo("true"));
//error fallback
Document doc = getDocumentFromXmlString(updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR));
Response response = updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR);
Document doc = getDocumentFromXmlString(response.readEntity(String.class));
Map<String, String> attrNamesAndValues = new HashMap<>();
attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
attrNamesAndValues.put("Location", "ERROR:ENDPOINT_NOT_SET");
@@ -231,7 +250,8 @@ public class InstallationTest extends AbstractClientTest {
//fallback to adminUrl
updater.setAdminUrl("https://admin-url").update();
assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientResourcePath(samlClientId), ResourceType.CLIENT);
doc = getDocumentFromXmlString(updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR));
response = updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR);
doc = getDocumentFromXmlString(response.readEntity(String.class));
attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
attrNamesAndValues.put("Location", "https://admin-url");
assertElements(doc, METADATA_NSURI.get(), "SingleLogoutService", attrNamesAndValues);
@@ -246,7 +266,8 @@ public class InstallationTest extends AbstractClientTest {
.update();
assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientResourcePath(samlClientId), ResourceType.CLIENT);
doc = getDocumentFromXmlString(updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR));
response = updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR);
doc = getDocumentFromXmlString(response.readEntity(String.class));
attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
attrNamesAndValues.put("Location", "https://saml-logout-post-url");
assertElements(doc, METADATA_NSURI.get(), "SingleLogoutService", attrNamesAndValues);
@@ -268,7 +289,8 @@ public class InstallationTest extends AbstractClientTest {
assertThat(updater.getResource().toRepresentation().getAttributes().get(SamlConfigAttributes.SAML_FORCE_POST_BINDING), equalTo("false"));
//error fallback
Document doc = getDocumentFromXmlString(updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR));
Response response = updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR);
Document doc = getDocumentFromXmlString(response.readEntity(String.class));
Map<String, String> attrNamesAndValues = new HashMap<>();
attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
attrNamesAndValues.put("Location", "ERROR:ENDPOINT_NOT_SET");
@@ -279,7 +301,8 @@ public class InstallationTest extends AbstractClientTest {
//fallback to adminUrl
updater.setAdminUrl("https://admin-url").update();
assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientResourcePath(samlClientId), ResourceType.CLIENT);
doc = getDocumentFromXmlString(updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR));
response = updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR);
doc = getDocumentFromXmlString(response.readEntity(String.class));
attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
attrNamesAndValues.put("Location", "https://admin-url");
assertElements(doc, METADATA_NSURI.get(), "SingleLogoutService", attrNamesAndValues);
@@ -293,7 +316,8 @@ public class InstallationTest extends AbstractClientTest {
.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "https://saml-logout-redirect-url")
.update();
assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientResourcePath(samlClientId), ResourceType.CLIENT);
doc = getDocumentFromXmlString(updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR));
response = updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR);
doc = getDocumentFromXmlString(response.readEntity(String.class));
attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
attrNamesAndValues.put("Location", "https://saml-logout-redirect-url");
assertElements(doc, METADATA_NSURI.get(), "SingleLogoutService", attrNamesAndValues);
@@ -305,6 +329,43 @@ public class InstallationTest extends AbstractClientTest {
assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientResourcePath(samlClientId), ResourceType.CLIENT);
}
@Test
public void testPemsInModAuthMellonExportShouldBeFormattedInRfc7468() throws IOException {
Response response = samlClient.getInstallationProvider("mod-auth-mellon");
byte[] result = response.readEntity(byte[].class);
String clientPrivateKey = null;
String clientCert = null;
try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(result))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.getName().endsWith("client-private-key.pem")) {
clientPrivateKey = new String(zis.readAllBytes(), StandardCharsets.US_ASCII);
}
else if (entry.getName().endsWith("client-cert.pem")) {
clientCert = new String(zis.readAllBytes(), StandardCharsets.US_ASCII);
}
}
}
assertNotNull(clientPrivateKey);
assertNotNull(clientCert);
assertRfc7468PrivateKey(clientPrivateKey);
assertRfc7468Cert(clientCert);
}
private void assertRfc7468PrivateKey(String result) {
assertTrue(result.startsWith("-----BEGIN PRIVATE KEY-----"));
assertTrue(result.endsWith("-----END PRIVATE KEY-----"));
result.lines().forEach(line -> assertTrue(line.length() <= 64));
}
private void assertRfc7468Cert(String result) {
assertTrue(result.startsWith("-----BEGIN CERTIFICATE-----"));
assertTrue(result.endsWith("-----END CERTIFICATE-----"));
result.lines().forEach(line -> assertTrue(line.length() <= 64));
}
private Document getDocumentFromXmlString(String xml) throws SAXException, ParserConfigurationException, IOException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);