KEYCLOAK-19143 Split note for broker and SP SAML request ID

This commit is contained in:
Hynek Mlnarik 2021-09-02 21:01:20 +02:00 committed by Hynek Mlnařík
parent 0c64d32b9b
commit 4518b3d3d1
5 changed files with 66 additions and 11 deletions

View file

@ -445,7 +445,7 @@ public class SAMLEndpoint {
} }
// Validate InResponseTo attribute: must match the generated request ID // Validate InResponseTo attribute: must match the generated request ID
String expectedRequestId = authSession.getClientNote(SamlProtocol.SAML_REQUEST_ID); String expectedRequestId = authSession.getClientNote(SamlProtocol.SAML_REQUEST_ID_BROKER);
final boolean inResponseToValidationSuccess = validateInResponseToAttribute(responseType, expectedRequestId); final boolean inResponseToValidationSuccess = validateInResponseToAttribute(responseType, expectedRequestId);
if (!inResponseToValidationSuccess) if (!inResponseToValidationSuccess)
{ {

View file

@ -189,7 +189,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
} }
// Save the current RequestID in the Auth Session as we need to verify it against the ID returned from the IdP // Save the current RequestID in the Auth Session as we need to verify it against the ID returned from the IdP
request.getAuthenticationSession().setClientNote(SamlProtocol.SAML_REQUEST_ID, authnRequest.getID()); request.getAuthenticationSession().setClientNote(SamlProtocol.SAML_REQUEST_ID_BROKER, authnRequest.getID());
if (postBinding) { if (postBinding) {
return binding.postBinding(authnRequestBuilder.toDocument()).request(destinationUrl); return binding.postBinding(authnRequestBuilder.toDocument()).request(destinationUrl);

View file

@ -122,6 +122,7 @@ public class SamlProtocol implements LoginProtocol {
public static final String SAML_SOAP_BINDING = "soap"; public static final String SAML_SOAP_BINDING = "soap";
public static final String SAML_REDIRECT_BINDING = "get"; public static final String SAML_REDIRECT_BINDING = "get";
public static final String SAML_REQUEST_ID = "SAML_REQUEST_ID"; public static final String SAML_REQUEST_ID = "SAML_REQUEST_ID";
public static final String SAML_REQUEST_ID_BROKER = "SAML_REQUEST_ID_BROKER";
public static final String SAML_LOGOUT_BINDING = "saml.logout.binding"; public static final String SAML_LOGOUT_BINDING = "saml.logout.binding";
public static final String SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO = "saml.logout.addExtensionsElementWithKeyInfo"; public static final String SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO = "saml.logout.addExtensionsElementWithKeyInfo";
public static final String SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER = "SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER"; public static final String SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER = "SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER";

View file

@ -18,6 +18,7 @@ package org.keycloak.testsuite.saml;
import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.authentication.authenticators.broker.IdpReviewProfileAuthenticatorFactory; import org.keycloak.authentication.authenticators.broker.IdpReviewProfileAuthenticatorFactory;
import org.keycloak.broker.saml.SAMLIdentityProviderConfig; import org.keycloak.broker.saml.SAMLIdentityProviderConfig;
import org.keycloak.broker.saml.SAMLIdentityProviderFactory; import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
@ -56,6 +57,7 @@ import java.security.KeyPair;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.Status;
import javax.xml.datatype.XMLGregorianCalendar; import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName; import javax.xml.namespace.QName;
@ -69,8 +71,9 @@ import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertThat; import static org.hamcrest.Matchers.is;
import static org.keycloak.saml.SignatureAlgorithm.RSA_SHA1; import static org.keycloak.saml.SignatureAlgorithm.RSA_SHA1;
import static org.keycloak.testsuite.saml.AbstractSamlTest.REALM_NAME; import static org.keycloak.testsuite.saml.AbstractSamlTest.REALM_NAME;
import static org.keycloak.testsuite.saml.AbstractSamlTest.SAML_ASSERTION_CONSUMER_URL_SALES_POST; import static org.keycloak.testsuite.saml.AbstractSamlTest.SAML_ASSERTION_CONSUMER_URL_SALES_POST;
@ -179,6 +182,47 @@ public class BrokerTest extends AbstractSamlTest {
} }
} }
@Test
public void testInResponseToSetCorrectly() throws IOException {
final RealmResource realm = adminClient.realm(REALM_NAME);
try (IdentityProviderCreator idp = new IdentityProviderCreator(realm, addIdentityProvider("https://saml.idp/saml"))) {
AtomicReference<String> serviceProvidersId = new AtomicReference<>();
SAMLDocumentHolder samlResponse = new SamlClientBuilder()
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST)
.transformObject(ar -> {
serviceProvidersId.set(ar.getID());
return ar;
})
.build()
.login().idp(SAML_BROKER_ALIAS).build()
// Virtually perform login at IdP (return artificial SAML response)
.processSamlResponse(REDIRECT)
.transformObject(this::createAuthnResponse)
.targetAttributeSamlResponse()
.targetUri(getSamlBrokerUrl(REALM_NAME))
.build()
.followOneRedirect() // first-broker-login
.updateProfile().username("userInResponseTo").email("f@g.h").firstName("a").lastName("b").build()
.followOneRedirect() // after-first-broker-login
.getSamlResponse(POST);
assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
assertThat(((ResponseType) samlResponse.getSamlObject()).getInResponseTo(), is(serviceProvidersId.get()));
} finally {
clearUsers(realm);
}
}
private void clearUsers(final RealmResource realm) {
realm.users().list().stream()
.map(UserRepresentation::getId)
.map(realm.users()::get)
.forEach(UserResource::remove);
}
@Test @Test
public void testNoNameIDAndPrincipalFromAttribute() throws IOException { public void testNoNameIDAndPrincipalFromAttribute() throws IOException {
final String userName = "newUser-" + UUID.randomUUID(); final String userName = "newUser-" + UUID.randomUUID();

View file

@ -17,9 +17,14 @@
package org.keycloak.testsuite.saml; package org.keycloak.testsuite.saml;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.saml.SAMLRequestParser;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.testsuite.util.Matchers;
import org.keycloak.testsuite.util.SamlClient; import org.keycloak.testsuite.util.SamlClient;
import org.keycloak.testsuite.util.saml.LoginBuilder; import org.keycloak.testsuite.util.saml.LoginBuilder;
import org.keycloak.testsuite.utils.io.IOUtil; import org.keycloak.testsuite.utils.io.IOUtil;
@ -43,6 +48,8 @@ import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.keycloak.testsuite.util.SamlClient.*; import static org.keycloak.testsuite.util.SamlClient.*;
/** /**
@ -55,20 +62,22 @@ public class ConcurrentAuthnRequestTest extends AbstractSamlTest {
public static final int ITERATIONS = 10000; public static final int ITERATIONS = 10000;
public static final int CONCURRENT_THREADS = 5; public static final int CONCURRENT_THREADS = 5;
private static void loginRepeatedly(UserRepresentation user, URI samlEndpoint, private void loginRepeatedly(UserRepresentation user, URI samlEndpoint,
Document samlRequest, String relayState, Binding requestBinding) { String relayState, Binding requestBinding) {
CloseableHttpResponse response = null; CloseableHttpResponse response = null;
SamlClient.RedirectStrategyWithSwitchableFollowRedirect strategy = new SamlClient.RedirectStrategyWithSwitchableFollowRedirect(); SamlClient.RedirectStrategyWithSwitchableFollowRedirect strategy = new SamlClient.RedirectStrategyWithSwitchableFollowRedirect();
ExecutorService threadPool = Executors.newFixedThreadPool(CONCURRENT_THREADS); ExecutorService threadPool = Executors.newFixedThreadPool(CONCURRENT_THREADS);
try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) { try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) {
HttpUriRequest post = requestBinding.createSamlUnsignedRequest(samlEndpoint, relayState, samlRequest);
Collection<Callable<Void>> futures = new LinkedList<>(); Collection<Callable<Void>> futures = new LinkedList<>();
for (int i = 0; i < ITERATIONS; i ++) { for (int i = 0; i < ITERATIONS; i ++) {
final int j = i; final int j = i;
AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME);
Document samlRequest = SAML2Request.convert(loginRep);
HttpUriRequest post = requestBinding.createSamlUnsignedRequest(samlEndpoint, relayState, samlRequest);
Callable<Void> f = () -> { Callable<Void> f = () -> {
performLogin(post, samlEndpoint, relayState, samlRequest, response, client, user, strategy); performLogin(post, samlEndpoint, relayState, loginRep.getID(), samlRequest, response, client, user, strategy);
return null; return null;
}; };
futures.add(f); futures.add(f);
@ -81,7 +90,7 @@ public class ConcurrentAuthnRequestTest extends AbstractSamlTest {
} }
public static void performLogin(HttpUriRequest post, URI samlEndpoint, String relayState, public static void performLogin(HttpUriRequest post, URI samlEndpoint, String relayState,
Document samlRequest, CloseableHttpResponse response, final CloseableHttpClient client, String requestId, Document samlRequest, CloseableHttpResponse response, final CloseableHttpClient client,
UserRepresentation user, UserRepresentation user,
RedirectStrategyWithSwitchableFollowRedirect strategy) { RedirectStrategyWithSwitchableFollowRedirect strategy) {
try { try {
@ -95,6 +104,9 @@ public class ConcurrentAuthnRequestTest extends AbstractSamlTest {
strategy.setRedirectable(false); strategy.setRedirectable(false);
response = client.execute(loginRequest, context); response = client.execute(loginRequest, context);
SAMLDocumentHolder parseResponsePostBinding = SAMLRequestParser.parseResponsePostBinding(EntityUtils.toString(response.getEntity()));
assertThat(parseResponsePostBinding.getSamlObject(), Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
assertThat(((ResponseType) parseResponsePostBinding.getSamlObject()).getInResponseTo(), is(requestId));
response.close(); response.close();
} catch (Exception ex) { } catch (Exception ex) {
throw new RuntimeException(ex); throw new RuntimeException(ex);
@ -117,9 +129,7 @@ public class ConcurrentAuthnRequestTest extends AbstractSamlTest {
} }
private void testLogin(Binding requestBinding) throws Exception { private void testLogin(Binding requestBinding) throws Exception {
AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME); loginRepeatedly(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), null, requestBinding);
Document samlRequest = SAML2Request.convert(loginRep);
loginRepeatedly(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), samlRequest, null, requestBinding);
} }
@Test @Test