8000 Add SAML broker regression tests for incomplete subject confirmation by atiqur-rahman-pro · Pull Request #54075 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
<apacheds.codec.version>2.1.8</apacheds.codec.version>
<kerby.version>2.1.2</kerby.version>
<google.zxing.version>3.4.0</google.zxing.version>
<jsoup.version>1.23.1</jsoup.version>
<freemarker.version>2.3.35</freemarker.version>
<sundrio.version>0.200.0</sundrio.version>
<antlr4.version>4.13.2</antlr4.version>
Expand Down Expand Up @@ -464,6 +465,11 @@
<artifactId>snakeyaml</artifactId>
<version>${org.yaml.snakeyaml.version}</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>${jsoup.version}</version>
</dependency>

<!--JAKARTA-->
<dependency>
Expand Down
5 changes: 5 additions & 0 deletions tests/base/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@
<artifactId>jboss-logmanager</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,135 @@
package org.keycloak.tests.broker.saml;

import java.util.function.Consumer;

import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.https.InjectCertificates;
import org.keycloak.testframework.https.ManagedCertificates;
import org.keycloak.tests.broker.AbstractKcSamlBrokerTest;
import org.keycloak.tests.saml.SamlClient;
import org.keycloak.tests.saml.SamlClientBuilder;

import org.junit.jupiter.api.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import static org.junit.jupiter.api.Assertions.assertEquals;

@KeycloakIntegrationTest
public class KcSamlBrokerTest extends AbstractKcSamlBrokerTest {

private static final String ASSERTION_NS = "urn:oasis:names:tc:SAML:2.0:assertion";
private static final String BEARER_METHOD = "urn:oasis:names:tc:SAML:2.0:cm:bearer";

@InjectCertificates
ManagedCertificates certificates;

@Test
public void testRejectsSamlResponseWithoutSubject() throws Exception {
assertInvalidSamlResponse(this::removeSubject);
}

@Test
public void testRejectsSamlResponseWithoutBearerSubjectConfirmation() throws Exception {
assertInvalidSamlResponse(this::removeBearerConfirmation);
}

@Test
public void testRejectsBearerSubjectConfirmationWithoutData() throws Exception {
assertInvalidSamlResponse(this::removeSubjectConfirmationData);
}

@Test
public void testRejectsBearerSubjectConfirmationWithoutNotOnOrAfter() throws Exception {
assertInvalidSamlResponse(this::removeNotOnOrAfter);
}

@Test
public void testRejectsBearerSubjectConfirmationWithoutRecipient() throws Exception {
assertInvalidSamlResponse(this::removeRecipient);
}

private void assertInvalidSamlResponse(Consumer<Document> mutator) throws Exception {
new SamlClientBuilder(certificates)
.navigateTo(oauth.loginForm().param("kc_idp_hint", IDP_SAML_ALIAS).build())
.processSamlResponse(SamlClient.Binding.POST)
.targetAttributeSamlRequest()
.build()
.login().user(getUserLogin(), getUserPassword()).build()
.processSamlResponse(SamlClient.Binding.POST)
.transformDocument(mutator)
.build()
.execute(response -> assertEquals(400, response.getStatusLine().getStatusCode(),
"Malformed broker SAML response should be rejected"));
}
Comment on lines +53 to +65

private void removeSubject(Document document) {
Node assertion = document.getElementsByTagNameNS(ASSERTION_NS, "Assertion").item(0);
Node subject = firstChild(assertion, "Subject");
assertion.removeChild(subject);
}

private void removeBearerConfirmation(Document document) {
Node assertion = document.getElementsByTagNameNS(ASSERTION_NS, "Assertion").item(0);
Node subject = firstChild(assertion, "Subject");
Node confirmation = findBearerConfirmation(subject);
subject.removeChild(confirmation);
}

private void removeSubjectConfirmationData(Document document) {
Node assertion = document.getElementsByTagNameNS(ASSERTION_NS, "Assertion").item(0);
Node subject = firstChild(assertion, "Subject");
Node confirmation = findBearerConfirmation(subject);
Node data = firstChild(confirmation, "SubjectConfirmationData");
confirmation.removeChild(data);
}

private void removeNotOnOrAfter(Document document) {
Node assertion = document.getElementsByTagNameNS(ASSERTION_NS, "Assertion").item(0);
Node subject = firstChild(assertion, "Subject");
Node confirmation = findBearerConfirmation(subject);
Node data = firstChild(confirmation, "SubjectConfirmationData");
data.getAttributes().removeNamedItem("NotOnOrAfter");
}

private void removeRecipient(Document document) {
Node assertion = document.getElementsByTagNameNS(ASSERTION_NS, "Assertion").item(0);
Node subject = firstChild(assertion, "Subject");
Node confirmation = findBearerConfirmation(subject);
Node data = firstChild(confirmation, "SubjectConfirmationData");
data.getAttributes().removeNamedItem("Recipient");
}

private static Node findBearerConfirmation(Node subject) {
Node confirmation = firstChild(subject, "SubjectConfirmation");
while (confirmation != null && (confirmation.getAttributes().getNamedItem("Method") == null
|| !BEARER_METHOD.equals(confirmation.getAttributes().getNamedItem("Method").getNodeValue()))) {
confirmation = nextSibling(confirmation, "SubjectConfirmation");
}
if (confirmation == null) {
throw new IllegalStateException("Missing Bearer SubjectConfirmation (Method=" + BEARER_METHOD + ")");
}
return confirmation;
}

private static Node firstChild(Node parent, String localName) {
NodeList children = parent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (localName.equals(child.getLocalName())) {
return child;
}
}
throw new IllegalStateException("Missing SAML element " + localName);
}

private static Node nextSibling(Node node, String localName) {
for (Node sibling = node.getNextSibling(); sibling != null; sibling = sibling.getNextSibling()) {
if (localName.equals(sibling.getLocalName())) {
return sibling;
}
}
return null;
}
}
131 changes: 131 additions & 0 deletions tests/base/src/test/java/org/keycloak/tests/saml/LoginBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright 2017 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.tests.saml;

import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.LinkedList;
import java.util.List;

import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

public class LoginBuilder implements SamlClient.Step {

private final SamlClientBuilder clientBuilder;
private String username;
private String password;
private String idpAlias;

public LoginBuilder(SamlClientBuilder clientBuilder) {
this.clientBuilder = clientBuilder;
}

public SamlClientBuilder build() {
return this.clientBuilder;
}

public LoginBuilder user(String username, String password) {
this.username = username;
this.password = password;
return this;
}

public LoginBuilder idp(String idpAlias) {
this.idpAlias = idpAlias;
return this;
}

@Override
public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
assertEquals(HttpStatus.SC_OK, currentResponse.getStatusLine().getStatusCode());
String loginPageText = EntityUtils.toString(currentResponse.getEntity(), StandardCharsets.UTF_8);

if (idpAlias != null) {
org.jsoup.nodes.Document doc = Jsoup.parse(loginPageText);
Element socialLink = doc.getElementById("social-" + this.idpAlias);
assertNotNull(socialLink, "Unknown idp: " + this.idpAlias);
String link = socialLink.attr("href");
assertNotNull(link, "Invalid idp link: " + this.idpAlias);
return new HttpGet(currentURI.resolve(link));
}

return handleLoginPage(loginPageText, currentURI);
}

private HttpUriRequest handleLoginPage(String loginPage, URI currentURI) {
org.jsoup.nodes.Document doc = Jsoup.parse(loginPage);
for (Element form : doc.getElementsByTag("form")) {
String method = form.attr("method");
String action = form.attr("action");
boolean isPost = method != null && "post".equalsIgnoreCase(method);

List<NameValuePair> parameters = new LinkedList<>();
for (Element input : form.getElementsByTag("input")) {
String id = input.id();
String name = input.attr("name");
if ("username".equalsIgnoreCase(id) || "username".equalsIgnoreCase(name)) {
if (name.isEmpty()) {
throw new IllegalStateException("Username input has no name attribute");
}
parameters.add(new BasicNameValuePair(name, username));
} else if ("password".equalsIgnoreCase(id) || "password".equalsIgnoreCase(name)) {
if (name.isEmpty()) {
throw new IllegalStateException("Password input has no name attribute");
}
parameters.add(new BasicNameValuePair(name, password));
} else if (name != null && !name.isEmpty()) {
parameters.add(new BasicNameValuePair(name, input.val()));
}
}

URI targetUri = currentURI.resolve(action);
if (isPost) {
HttpPost post = new HttpPost(targetUri);
post.setEntity(new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8));
return post;
} else {
URIBuilder b = new URIBuilder(targetUri);
for (NameValuePair parameter : parameters) {
b.addParameter(parameter.getName(), parameter.getValue());
}
try {
return new HttpGet(b.build());
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}
throw new IllegalArgumentException("Invalid login form: " + loginPage);
}
}
Loading
Loading
0