Merge pull request #3705 from hmlnarik/KEYCLOAK-3950-Tests-of-SAML-AuthnRequests-NameID-format-variants
KEYCLOAK-3950 - Tests for SAML Name ID format variants in AuthnRequest
This commit is contained in:
commit
08aeb13804
6 changed files with 550 additions and 9 deletions
|
@ -243,7 +243,7 @@ public class SAML2Request {
|
||||||
*
|
*
|
||||||
* @throws ConfigurationException
|
* @throws ConfigurationException
|
||||||
*/
|
*/
|
||||||
public LogoutRequestType createLogoutRequest(String issuer) throws ConfigurationException {
|
public static LogoutRequestType createLogoutRequest(String issuer) throws ConfigurationException {
|
||||||
LogoutRequestType lrt = new LogoutRequestType(IDGenerator.create("ID_"), XMLTimeUtil.getIssueInstant());
|
LogoutRequestType lrt = new LogoutRequestType(IDGenerator.create("ID_"), XMLTimeUtil.getIssueInstant());
|
||||||
|
|
||||||
// Create an issuer
|
// Create an issuer
|
||||||
|
@ -266,7 +266,7 @@ public class SAML2Request {
|
||||||
* @throws ParsingException
|
* @throws ParsingException
|
||||||
* @throws ConfigurationException
|
* @throws ConfigurationException
|
||||||
*/
|
*/
|
||||||
public Document convert(RequestAbstractType rat) throws ProcessingException, ConfigurationException, ParsingException {
|
public static Document convert(RequestAbstractType rat) throws ProcessingException, ConfigurationException, ParsingException {
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
|
||||||
SAMLRequestWriter writer = new SAMLRequestWriter(StaxUtil.getXMLStreamWriter(bos));
|
SAMLRequestWriter writer = new SAMLRequestWriter(StaxUtil.getXMLStreamWriter(bos));
|
||||||
|
@ -290,7 +290,7 @@ public class SAML2Request {
|
||||||
* @throws ParsingException
|
* @throws ParsingException
|
||||||
* @throws ConfigurationException
|
* @throws ConfigurationException
|
||||||
*/
|
*/
|
||||||
public Document convert(ResponseType responseType) throws ProcessingException, ParsingException, ConfigurationException {
|
public static Document convert(ResponseType responseType) throws ProcessingException, ParsingException, ConfigurationException {
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
SAMLResponseWriter writer = new SAMLResponseWriter(StaxUtil.getXMLStreamWriter(baos));
|
SAMLResponseWriter writer = new SAMLResponseWriter(StaxUtil.getXMLStreamWriter(baos));
|
||||||
writer.write(responseType);
|
writer.write(responseType);
|
||||||
|
@ -307,7 +307,7 @@ public class SAML2Request {
|
||||||
*
|
*
|
||||||
* @throws ProcessingException
|
* @throws ProcessingException
|
||||||
*/
|
*/
|
||||||
public void marshall(RequestAbstractType requestType, OutputStream os) throws ProcessingException {
|
public static void marshall(RequestAbstractType requestType, OutputStream os) throws ProcessingException {
|
||||||
SAMLRequestWriter samlRequestWriter = new SAMLRequestWriter(StaxUtil.getXMLStreamWriter(os));
|
SAMLRequestWriter samlRequestWriter = new SAMLRequestWriter(StaxUtil.getXMLStreamWriter(os));
|
||||||
if (requestType instanceof AuthnRequestType) {
|
if (requestType instanceof AuthnRequestType) {
|
||||||
samlRequestWriter.write((AuthnRequestType) requestType);
|
samlRequestWriter.write((AuthnRequestType) requestType);
|
||||||
|
@ -325,7 +325,7 @@ public class SAML2Request {
|
||||||
*
|
*
|
||||||
* @throws ProcessingException
|
* @throws ProcessingException
|
||||||
*/
|
*/
|
||||||
public void marshall(RequestAbstractType requestType, Writer writer) throws ProcessingException {
|
public static void marshall(RequestAbstractType requestType, Writer writer) throws ProcessingException {
|
||||||
SAMLRequestWriter samlRequestWriter = new SAMLRequestWriter(StaxUtil.getXMLStreamWriter(writer));
|
SAMLRequestWriter samlRequestWriter = new SAMLRequestWriter(StaxUtil.getXMLStreamWriter(writer));
|
||||||
if (requestType instanceof AuthnRequestType) {
|
if (requestType instanceof AuthnRequestType) {
|
||||||
samlRequestWriter.write((AuthnRequestType) requestType);
|
samlRequestWriter.write((AuthnRequestType) requestType);
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://localhost:8081/auth/realms/saml-demo</saml:Issuer>
|
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://localhost:8081/auth/realms/saml-demo</saml:Issuer>
|
||||||
<samlp:Extensions>
|
<samlp:Extensions>
|
||||||
<kckey:KeyInfo xmlns:kckey="urn:keycloak:ext:key:1.0" MessageSigningKeyId="FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"/>
|
<kckey:KeyInfo xmlns:kckey="urn:keycloak:ext:key:1.0" MessageSigningKeyId="FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"/>
|
||||||
<what:ever xmlns:what="urn:keycloak:ext:what:1.0" what="ever"/>
|
<what:ever xmlns:what="urn:keycloak:ext:what:1.0" what="ever">
|
||||||
|
<nested><element>text contents</element></nested>
|
||||||
|
</what:ever>
|
||||||
</samlp:Extensions>
|
</samlp:Extensions>
|
||||||
<samlp:Status>
|
<samlp:Status>
|
||||||
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
|
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
|
||||||
|
|
|
@ -0,0 +1,200 @@
|
||||||
|
/*
|
||||||
|
* 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.testsuite.saml;
|
||||||
|
|
||||||
|
import org.keycloak.admin.client.resource.ClientResource;
|
||||||
|
import org.keycloak.admin.client.resource.ClientsResource;
|
||||||
|
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.NameIDPolicyType;
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||||
|
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
||||||
|
import org.keycloak.protocol.saml.SamlProtocol;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
|
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
|
||||||
|
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||||
|
import org.keycloak.services.resources.RealmsResource;
|
||||||
|
import org.keycloak.testsuite.AbstractAuthTest;
|
||||||
|
import org.keycloak.testsuite.util.SamlClient;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
import javax.ws.rs.core.UriBuilderException;
|
||||||
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
|
import org.apache.http.client.methods.HttpUriRequest;
|
||||||
|
import org.apache.http.client.protocol.HttpClientContext;
|
||||||
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
import org.apache.http.impl.client.HttpClientBuilder;
|
||||||
|
import org.apache.http.util.EntityUtils;
|
||||||
|
import org.hamcrest.Matcher;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.*;
|
||||||
|
import static org.keycloak.testsuite.util.SamlClient.*;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.keycloak.testsuite.util.IOUtil.loadRealm;
|
||||||
|
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class AuthnRequestNameIdFormatTest extends AbstractAuthTest {
|
||||||
|
|
||||||
|
private static final String REALM_NAME = "demo";
|
||||||
|
|
||||||
|
private static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST = "http://localhost:8080/sales-post/";
|
||||||
|
private static final String SAML_CLIENT_ID_SALES_POST = "http://localhost:8081/sales-post/";
|
||||||
|
|
||||||
|
public static SAMLDocumentHolder login(UserRepresentation user, URI samlEndpoint,
|
||||||
|
Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding) {
|
||||||
|
CloseableHttpResponse response = null;
|
||||||
|
SamlClient.RedirectStrategyWithSwitchableFollowRedirect strategy = new SamlClient.RedirectStrategyWithSwitchableFollowRedirect();
|
||||||
|
try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) {
|
||||||
|
HttpClientContext context = HttpClientContext.create();
|
||||||
|
|
||||||
|
HttpUriRequest post = requestBinding.createSamlRequest(samlEndpoint, relayState, samlRequest);
|
||||||
|
response = client.execute(post, context);
|
||||||
|
|
||||||
|
assertThat(response, statusCodeIsHC(Response.Status.OK));
|
||||||
|
String loginPageText = EntityUtils.toString(response.getEntity(), "UTF-8");
|
||||||
|
response.close();
|
||||||
|
|
||||||
|
assertThat(loginPageText, containsString("login"));
|
||||||
|
|
||||||
|
HttpUriRequest loginRequest = handleLoginPage(user, loginPageText);
|
||||||
|
|
||||||
|
strategy.setRedirectable(false);
|
||||||
|
response = client.execute(loginRequest, context);
|
||||||
|
|
||||||
|
return expectedResponseBinding.extractResponse(response);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
} finally {
|
||||||
|
if (response != null) {
|
||||||
|
EntityUtils.consumeQuietly(response.getEntity());
|
||||||
|
try { response.close(); } catch (IOException ex) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||||
|
testRealms.add(loadRealm("/adapter-test/keycloak-saml/testsaml.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthnRequestType createLoginRequestDocument(String issuer, String assertionConsumerURL, String realmName) {
|
||||||
|
return SamlClient.createLoginRequestDocument(issuer, assertionConsumerURL, getAuthServerSamlEndpoint(realmName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private URI getAuthServerSamlEndpoint(String realm) throws IllegalArgumentException, UriBuilderException {
|
||||||
|
return RealmsResource
|
||||||
|
.protocolUrl(UriBuilder.fromUri(getAuthServerRoot()))
|
||||||
|
.build(realm, SamlProtocol.LOGIN_PROTOCOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testLoginWithNameIdPolicy(Binding requestBinding, Binding responseBinding, NameIDPolicyType nameIDPolicy, Matcher<String> nameIdMatcher) throws Exception {
|
||||||
|
AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME);
|
||||||
|
loginRep.setProtocolBinding(requestBinding.getBindingUri());
|
||||||
|
loginRep.setNameIDPolicy(nameIDPolicy);
|
||||||
|
|
||||||
|
Document samlRequest = SAML2Request.convert(loginRep);
|
||||||
|
SAMLDocumentHolder res = login(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), samlRequest, null, requestBinding, responseBinding);
|
||||||
|
|
||||||
|
assertThat(res.getSamlObject(), notNullValue());
|
||||||
|
assertThat(res.getSamlObject(), instanceOf(ResponseType.class));
|
||||||
|
|
||||||
|
ResponseType rt = (ResponseType) res.getSamlObject();
|
||||||
|
assertThat(rt.getAssertions(), not(empty()));
|
||||||
|
assertThat(rt.getAssertions().get(0).getAssertion().getSubject().getSubType().getBaseID(), instanceOf(NameIDType.class));
|
||||||
|
NameIDType nameId = (NameIDType) rt.getAssertions().get(0).getAssertion().getSubject().getSubType().getBaseID();
|
||||||
|
assertThat(nameId.getValue(), nameIdMatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPostLoginNameIdPolicyUnspecified() throws Exception {
|
||||||
|
NameIDPolicyType nameIdPolicy = new NameIDPolicyType();
|
||||||
|
nameIdPolicy.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get()));
|
||||||
|
testLoginWithNameIdPolicy(Binding.POST, Binding.POST, nameIdPolicy, is("bburke"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPostLoginNameIdPolicyEmail() throws Exception {
|
||||||
|
NameIDPolicyType nameIdPolicy = new NameIDPolicyType();
|
||||||
|
nameIdPolicy.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get()));
|
||||||
|
testLoginWithNameIdPolicy(Binding.POST, Binding.POST, nameIdPolicy, is("bburke@redhat.com"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPostLoginNameIdPolicyPersistent() throws Exception {
|
||||||
|
NameIDPolicyType nameIdPolicy = new NameIDPolicyType();
|
||||||
|
nameIdPolicy.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get()));
|
||||||
|
testLoginWithNameIdPolicy(Binding.POST, Binding.POST, nameIdPolicy, startsWith("G-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPostLoginNoNameIdPolicyUnset() throws Exception {
|
||||||
|
testLoginWithNameIdPolicy(Binding.POST, Binding.POST, null, is("bburke"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRedirectLoginNameIdPolicyUnspecified() throws Exception {
|
||||||
|
NameIDPolicyType nameIdPolicy = new NameIDPolicyType();
|
||||||
|
nameIdPolicy.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get()));
|
||||||
|
testLoginWithNameIdPolicy(Binding.REDIRECT, Binding.REDIRECT, nameIdPolicy, is("bburke"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRedirectLoginNameIdPolicyEmail() throws Exception {
|
||||||
|
NameIDPolicyType nameIdPolicy = new NameIDPolicyType();
|
||||||
|
nameIdPolicy.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get()));
|
||||||
|
testLoginWithNameIdPolicy(Binding.REDIRECT, Binding.REDIRECT, nameIdPolicy, is("bburke@redhat.com"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRedirectLoginNameIdPolicyPersistent() throws Exception {
|
||||||
|
NameIDPolicyType nameIdPolicy = new NameIDPolicyType();
|
||||||
|
nameIdPolicy.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get()));
|
||||||
|
testLoginWithNameIdPolicy(Binding.REDIRECT, Binding.REDIRECT, nameIdPolicy, startsWith("G-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRedirectLoginNoNameIdPolicyUnset() throws Exception {
|
||||||
|
testLoginWithNameIdPolicy(Binding.REDIRECT, Binding.REDIRECT, null, is("bburke"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRedirectLoginNoNameIdPolicyForcePostBinding() throws Exception {
|
||||||
|
ClientsResource clients = adminClient.realm(REALM_NAME).clients();
|
||||||
|
List<ClientRepresentation> foundClients = clients.findByClientId("http://localhost:8081/sales-post/");
|
||||||
|
assertThat(foundClients, hasSize(1));
|
||||||
|
ClientResource clientRes = clients.get(foundClients.get(0).getId());
|
||||||
|
ClientRepresentation client = clientRes.toRepresentation();
|
||||||
|
client.getAttributes().put(SamlConfigAttributes.SAML_FORCE_POST_BINDING, "true");
|
||||||
|
clientRes.update(client);
|
||||||
|
|
||||||
|
testLoginWithNameIdPolicy(Binding.REDIRECT, Binding.POST, null, is("bburke"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -16,12 +16,11 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.util;
|
package org.keycloak.testsuite.util;
|
||||||
|
|
||||||
import org.keycloak.testsuite.util.matchers.ResponseBodyMatcher;
|
import org.keycloak.testsuite.util.matchers.*;
|
||||||
import org.keycloak.testsuite.util.matchers.ResponseHeaderMatcher;
|
|
||||||
import org.keycloak.testsuite.util.matchers.ResponseStatusCodeMatcher;
|
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import org.apache.http.HttpResponse;
|
||||||
import org.hamcrest.Matcher;
|
import org.hamcrest.Matcher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -48,6 +47,15 @@ public class Matchers {
|
||||||
return new ResponseStatusCodeMatcher(matcher);
|
return new ResponseStatusCodeMatcher(matcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher on HTTP status code of a {@link Response} instance (HttpClient variant).
|
||||||
|
* @param matcher
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static Matcher<HttpResponse> statusCodeHC(Matcher<? extends Number> matcher) {
|
||||||
|
return new HttpResponseStatusCodeMatcher(matcher);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Matches when the HTTP status code of a {@link Response} instance is equal to the given code.
|
* Matches when the HTTP status code of a {@link Response} instance is equal to the given code.
|
||||||
* @param expectedStatusCode
|
* @param expectedStatusCode
|
||||||
|
@ -57,6 +65,15 @@ public class Matchers {
|
||||||
return new ResponseStatusCodeMatcher(org.hamcrest.Matchers.is(expectedStatusCode.getStatusCode()));
|
return new ResponseStatusCodeMatcher(org.hamcrest.Matchers.is(expectedStatusCode.getStatusCode()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches when the HTTP status code of a {@link Response} instance is equal to the given code (HttpClient variant).
|
||||||
|
* @param expectedStatusCode
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static Matcher<HttpResponse> statusCodeIsHC(Response.Status expectedStatusCode) {
|
||||||
|
return new HttpResponseStatusCodeMatcher(org.hamcrest.Matchers.is(expectedStatusCode.getStatusCode()));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Matches when the HTTP status code of a {@link Response} instance is equal to the given code.
|
* Matches when the HTTP status code of a {@link Response} instance is equal to the given code.
|
||||||
* @param expectedStatusCode
|
* @param expectedStatusCode
|
||||||
|
@ -66,6 +83,15 @@ public class Matchers {
|
||||||
return new ResponseStatusCodeMatcher(org.hamcrest.Matchers.is(expectedStatusCode));
|
return new ResponseStatusCodeMatcher(org.hamcrest.Matchers.is(expectedStatusCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches when the HTTP status code of a {@link Response} instance is equal to the given code (HttpClient variant).
|
||||||
|
* @param expectedStatusCode
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static Matcher<HttpResponse> statusCodeIsHC(int expectedStatusCode) {
|
||||||
|
return new HttpResponseStatusCodeMatcher(org.hamcrest.Matchers.is(expectedStatusCode));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Matches when the HTTP status code of a {@link Response} instance is equal to the given code.
|
* Matches when the HTTP status code of a {@link Response} instance is equal to the given code.
|
||||||
* @param expectedStatusCode
|
* @param expectedStatusCode
|
||||||
|
|
|
@ -0,0 +1,266 @@
|
||||||
|
/*
|
||||||
|
* 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.testsuite.util;
|
||||||
|
|
||||||
|
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.saml.BaseSAML2BindingBuilder;
|
||||||
|
import org.keycloak.saml.SAMLRequestParser;
|
||||||
|
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||||
|
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
|
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||||
|
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||||
|
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
|
||||||
|
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
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.utils.URLEncodedUtils;
|
||||||
|
import org.apache.http.impl.client.LaxRedirectStrategy;
|
||||||
|
import org.apache.http.message.BasicNameValuePair;
|
||||||
|
import org.apache.http.util.EntityUtils;
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.jsoup.select.Elements;
|
||||||
|
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.*;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.keycloak.testsuite.admin.Users.getPasswordOf;
|
||||||
|
import static org.keycloak.testsuite.util.Matchers.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class SamlClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SAML bindings and related HttpClient methods.
|
||||||
|
*/
|
||||||
|
public enum Binding {
|
||||||
|
POST {
|
||||||
|
@Override
|
||||||
|
public SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException {
|
||||||
|
assertThat(response, statusCodeIsHC(Response.Status.OK));
|
||||||
|
String responsePage = EntityUtils.toString(response.getEntity(), "UTF-8");
|
||||||
|
response.close();
|
||||||
|
return extractSamlResponseFromForm(responsePage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpPost createSamlRequest(URI samlEndpoint, String relayState, Document samlRequest) {
|
||||||
|
HttpPost post = new HttpPost(samlEndpoint);
|
||||||
|
|
||||||
|
List<NameValuePair> parameters = new LinkedList<>();
|
||||||
|
try {
|
||||||
|
parameters.add(
|
||||||
|
new BasicNameValuePair(GeneralConstants.SAML_REQUEST_KEY,
|
||||||
|
new BaseSAML2BindingBuilder()
|
||||||
|
.postBinding(samlRequest)
|
||||||
|
.encoded())
|
||||||
|
);
|
||||||
|
} catch (ProcessingException | ConfigurationException | IOException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
if (relayState != null) {
|
||||||
|
parameters.add(new BasicNameValuePair(GeneralConstants.RELAY_STATE, relayState));
|
||||||
|
}
|
||||||
|
|
||||||
|
UrlEncodedFormEntity formEntity;
|
||||||
|
try {
|
||||||
|
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
post.setEntity(formEntity);
|
||||||
|
|
||||||
|
return post;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URI getBindingUri() {
|
||||||
|
return URI.create(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
REDIRECT {
|
||||||
|
@Override
|
||||||
|
public SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException {
|
||||||
|
assertThat(response, statusCodeIsHC(Response.Status.FOUND));
|
||||||
|
String location = response.getFirstHeader("Location").getValue();
|
||||||
|
response.close();
|
||||||
|
return extractSamlResponseFromRedirect(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpGet createSamlRequest(URI samlEndpoint, String relayState, Document samlRequest) {
|
||||||
|
try {
|
||||||
|
URI requestURI = new BaseSAML2BindingBuilder()
|
||||||
|
.relayState(relayState)
|
||||||
|
.redirectBinding(samlRequest)
|
||||||
|
.requestURI(samlEndpoint.toString());
|
||||||
|
return new HttpGet(requestURI);
|
||||||
|
} catch (ProcessingException | ConfigurationException | IOException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URI getBindingUri() {
|
||||||
|
return URI.create(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public abstract SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException;
|
||||||
|
public abstract HttpUriRequest createSamlRequest(URI samlEndpoint, String relayState, Document samlRequest);
|
||||||
|
public abstract URI getBindingUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RedirectStrategyWithSwitchableFollowRedirect extends LaxRedirectStrategy {
|
||||||
|
|
||||||
|
public boolean redirectable = true;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isRedirectable(String method) {
|
||||||
|
return redirectable && super.isRedirectable(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRedirectable(boolean redirectable) {
|
||||||
|
this.redirectable = redirectable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts and parses value of SAMLResponse input field of a form present in the given page.
|
||||||
|
* @param responsePage HTML code of the page
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static SAMLDocumentHolder extractSamlResponseFromForm(String responsePage) {
|
||||||
|
org.jsoup.nodes.Document theResponsePage = Jsoup.parse(responsePage);
|
||||||
|
Elements samlResponses = theResponsePage.select("input[name=SAMLResponse]");
|
||||||
|
assertThat("Checking uniqueness of SAMLResponse input field in the page", samlResponses, hasSize(1));
|
||||||
|
|
||||||
|
Element respElement = samlResponses.first();
|
||||||
|
|
||||||
|
return SAMLRequestParser.parseResponsePostBinding(respElement.val());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts and parses value of SAMLResponse query parameter from the given URI.
|
||||||
|
* @param responseUri
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static SAMLDocumentHolder extractSamlResponseFromRedirect(String responseUri) {
|
||||||
|
List<NameValuePair> params = URLEncodedUtils.parse(URI.create(responseUri), "UTF-8");
|
||||||
|
|
||||||
|
String samlResponse = null;
|
||||||
|
for (NameValuePair param : params) {
|
||||||
|
if ("SAMLResponse".equals(param.getName())) {
|
||||||
|
assertThat(samlResponse, nullValue());
|
||||||
|
samlResponse = param.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SAMLRequestParser.parseResponseRedirectBinding(samlResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares a GET/POST request for logging the given user into the given login page. The login page is expected
|
||||||
|
* to have at least input fields with id "username" and "password".
|
||||||
|
* @param user
|
||||||
|
* @param loginPage
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static HttpUriRequest handleLoginPage(UserRepresentation user, String loginPage) {
|
||||||
|
String username = user.getUsername();
|
||||||
|
String password = getPasswordOf(user);
|
||||||
|
org.jsoup.nodes.Document theLoginPage = Jsoup.parse(loginPage);
|
||||||
|
|
||||||
|
List<NameValuePair> parameters = new LinkedList<>();
|
||||||
|
for (Element form : theLoginPage.getElementsByTag("form")) {
|
||||||
|
String method = form.attr("method");
|
||||||
|
String action = form.attr("action");
|
||||||
|
boolean isPost = method != null && "post".equalsIgnoreCase(method);
|
||||||
|
|
||||||
|
for (Element input : form.getElementsByTag("input")) {
|
||||||
|
if (Objects.equals(input.id(), "username")) {
|
||||||
|
parameters.add(new BasicNameValuePair(input.attr("name"), username));
|
||||||
|
} else if (Objects.equals(input.id(), "password")) {
|
||||||
|
parameters.add(new BasicNameValuePair(input.attr("name"), password));
|
||||||
|
} else {
|
||||||
|
parameters.add(new BasicNameValuePair(input.attr("name"), input.val()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPost) {
|
||||||
|
HttpPost res = new HttpPost(action);
|
||||||
|
|
||||||
|
UrlEncodedFormEntity formEntity;
|
||||||
|
try {
|
||||||
|
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
res.setEntity(formEntity);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
UriBuilder b = UriBuilder.fromPath(action);
|
||||||
|
for (NameValuePair parameter : parameters) {
|
||||||
|
b.queryParam(parameter.getName(), parameter.getValue());
|
||||||
|
}
|
||||||
|
return new HttpGet(b.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("Invalid login form: " + loginPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a SAML login request document with the given parameters. See SAML <AuthnRequest> description for more details.
|
||||||
|
* @param issuer
|
||||||
|
* @param assertionConsumerURL
|
||||||
|
* @param destination
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static AuthnRequestType createLoginRequestDocument(String issuer, String assertionConsumerURL, URI destination) {
|
||||||
|
try {
|
||||||
|
SAML2Request samlReq = new SAML2Request();
|
||||||
|
AuthnRequestType loginReq = samlReq.createAuthnRequestType(UUID.randomUUID().toString(), assertionConsumerURL, destination.toString(), issuer);
|
||||||
|
|
||||||
|
return loginReq;
|
||||||
|
} catch (ConfigurationException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 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.util.matchers;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import org.apache.http.HttpResponse;
|
||||||
|
import org.hamcrest.BaseMatcher;
|
||||||
|
import org.hamcrest.Description;
|
||||||
|
import org.hamcrest.Matcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher for matching status code of {@link Response} instance.
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class HttpResponseStatusCodeMatcher extends BaseMatcher<HttpResponse> {
|
||||||
|
|
||||||
|
private final Matcher<? extends Number> matcher;
|
||||||
|
|
||||||
|
public HttpResponseStatusCodeMatcher(Matcher<? extends Number> matcher) {
|
||||||
|
this.matcher = matcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(Object item) {
|
||||||
|
return (item instanceof HttpResponse) && this.matcher.matches(((HttpResponse) item).getStatusLine().getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void describeTo(Description description) {
|
||||||
|
description.appendText("response status code matches ").appendDescriptionOf(this.matcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue