KEYCLOAK-9992 Support for ARTIFACT binding in server to client communication
Co-authored-by: AlistairDoswald <alistair.doswald@elca.ch> Co-authored-by: harture <harture414@gmail.com> Co-authored-by: Michal Hajas <mhajas@redhat.com>
This commit is contained in:
parent
64ccbda5d5
commit
8b3e77bf81
86 changed files with 4900 additions and 240 deletions
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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.models.sessions.infinispan;
|
||||
|
||||
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
|
||||
import org.infinispan.commons.api.BasicCache;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.SamlArtifactSessionMappingModel;
|
||||
import org.keycloak.models.SamlArtifactSessionMappingStoreProvider;
|
||||
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
public class InfinispanSamlArtifactSessionMappingStoreProvider implements SamlArtifactSessionMappingStoreProvider {
|
||||
|
||||
public static final Logger logger = Logger.getLogger(InfinispanSamlArtifactSessionMappingStoreProvider.class);
|
||||
|
||||
private final Supplier<BasicCache<UUID, String[]>> cacheSupplier;
|
||||
|
||||
public InfinispanSamlArtifactSessionMappingStoreProvider(Supplier<BasicCache<UUID, String[]>> actionKeyCache) {
|
||||
this.cacheSupplier = actionKeyCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(String artifact, int lifespanSeconds, AuthenticatedClientSessionModel clientSessionModel) {
|
||||
try {
|
||||
BasicCache<UUID, String[]> cache = cacheSupplier.get();
|
||||
long lifespanMs = InfinispanUtil.toHotrodTimeMs(cache, Time.toMillis(lifespanSeconds));
|
||||
cache.put(UUID.nameUUIDFromBytes(artifact.getBytes(StandardCharsets.UTF_8)), new String[]{artifact, clientSessionModel.getUserSession().getId(), clientSessionModel.getClient().getId()}, lifespanMs, TimeUnit.MILLISECONDS);
|
||||
} catch (HotRodClientException re) {
|
||||
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debugf(re, "Failed adding artifact %s", artifact);
|
||||
}
|
||||
|
||||
throw re;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SamlArtifactSessionMappingModel get(String artifact) {
|
||||
try {
|
||||
BasicCache<UUID, String[]> cache = cacheSupplier.get();
|
||||
String[] existing = cache.get(UUID.nameUUIDFromBytes(artifact.getBytes(StandardCharsets.UTF_8)));
|
||||
if (existing == null || existing.length != 3) return null;
|
||||
if (!artifact.equals(existing[0])) return null; // Check
|
||||
return new SamlArtifactSessionMappingModel(existing[1], existing[2]);
|
||||
} catch (HotRodClientException re) {
|
||||
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
|
||||
// In case of lock conflict, we don't want to retry anyway as there was likely an attempt to remove the code from different place.
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debugf(re, "Failed when obtaining data for artifact %s", artifact);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(String artifact) {
|
||||
try {
|
||||
BasicCache<UUID, String[]> cache = cacheSupplier.get();
|
||||
if (cache.remove(UUID.nameUUIDFromBytes(artifact.getBytes(StandardCharsets.UTF_8))) == null) {
|
||||
logger.debugf("Artifact %s was already removed", artifact);
|
||||
}
|
||||
} catch (HotRodClientException re) {
|
||||
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
|
||||
// In case of lock conflict, we don't want to retry anyway as there was likely an attempt to remove the code from different place.
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debugf(re, "Failed to remove artifact %s", artifact);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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.models.sessions.infinispan;
|
||||
|
||||
import org.infinispan.commons.api.BasicCache;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.SamlArtifactSessionMappingStoreProvider;
|
||||
import org.keycloak.models.SamlArtifactSessionMappingStoreProviderFactory;
|
||||
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
public class InfinispanSamlArtifactSessionMappingStoreProviderFactory implements SamlArtifactSessionMappingStoreProviderFactory {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(InfinispanSamlArtifactSessionMappingStoreProviderFactory.class);
|
||||
|
||||
// Reuse "actionTokens" infinispan cache for now
|
||||
private volatile Supplier<BasicCache<UUID, String[]>> codeCache;
|
||||
|
||||
@Override
|
||||
public SamlArtifactSessionMappingStoreProvider create(KeycloakSession session) {
|
||||
lazyInit(session);
|
||||
return new InfinispanSamlArtifactSessionMappingStoreProvider(codeCache);
|
||||
}
|
||||
|
||||
private void lazyInit(KeycloakSession session) {
|
||||
if (codeCache == null) {
|
||||
synchronized (this) {
|
||||
if (codeCache == null) {
|
||||
this.codeCache = InfinispanSingleUseTokenStoreProviderFactory.getActionTokenCache(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "infinispan";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
#
|
||||
# Copyright 2021 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.
|
||||
#
|
||||
|
||||
org.keycloak.models.sessions.infinispan.InfinispanSamlArtifactSessionMappingStoreProviderFactory
|
|
@ -98,6 +98,8 @@ public interface GeneralConstants {
|
|||
|
||||
String SAML_RESPONSE_KEY = "SAMLResponse";
|
||||
|
||||
String SAML_ARTIFACT_KEY = "SAMLart";
|
||||
|
||||
String SAML_SIG_ALG_REQUEST_KEY = "SigAlg";
|
||||
|
||||
String SAML_SIGNATURE_REQUEST_KEY = "Signature";
|
||||
|
|
|
@ -88,6 +88,7 @@ public enum JBossSAMLURIConstants {
|
|||
SAML_HTTP_REDIRECT_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"),
|
||||
SAML_SOAP_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:SOAP"),
|
||||
SAML_PAOS_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:PAOS"),
|
||||
SAML_HTTP_ARTIFACT_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"),
|
||||
|
||||
SAML_11_NS("urn:oasis:names:tc:SAML:1.0:assertion"),
|
||||
|
||||
|
|
|
@ -48,6 +48,8 @@ import java.security.PublicKey;
|
|||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.keycloak.common.util.HtmlUtils.escapeAttribute;
|
||||
import static org.keycloak.saml.common.util.StringUtil.isNotNull;
|
||||
|
@ -311,7 +313,6 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
|
|||
}
|
||||
|
||||
public String buildHtml(String samlResponse, String actionUrl, boolean asRequest) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
String key = GeneralConstants.SAML_RESPONSE_KEY;
|
||||
|
||||
|
@ -319,32 +320,44 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
|
|||
key = GeneralConstants.SAML_REQUEST_KEY;
|
||||
}
|
||||
|
||||
Map<String, String> inputTypes = new HashMap<>();
|
||||
inputTypes.put(key, samlResponse);
|
||||
if (isNotNull(relayState)) {
|
||||
inputTypes.put(GeneralConstants.RELAY_STATE, relayState);
|
||||
}
|
||||
|
||||
return buildHtmlForm(actionUrl, inputTypes);
|
||||
}
|
||||
|
||||
public String buildHtmlForm(String actionUrl, Map<String, String> inputTypes) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
builder.append("<HTML>")
|
||||
.append("<HEAD>")
|
||||
.append("<HEAD>")
|
||||
|
||||
.append("<TITLE>Authentication Redirect</TITLE>")
|
||||
.append("</HEAD>")
|
||||
.append("<BODY Onload=\"document.forms[0].submit()\">")
|
||||
.append("<TITLE>Authentication Redirect</TITLE>")
|
||||
.append("</HEAD>")
|
||||
.append("<BODY Onload=\"document.forms[0].submit()\">")
|
||||
|
||||
.append("<FORM METHOD=\"POST\" ACTION=\"").append(actionUrl).append("\">")
|
||||
.append("<INPUT TYPE=\"HIDDEN\" NAME=\"").append(key).append("\"").append(" VALUE=\"").append(samlResponse).append("\"/>");
|
||||
.append("<FORM METHOD=\"POST\" ACTION=\"").append(actionUrl).append("\">");
|
||||
|
||||
builder.append("<p>Redirecting, please wait.</p>");
|
||||
|
||||
if (isNotNull(relayState)) {
|
||||
builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"RelayState\" " + "VALUE=\"").append(escapeAttribute(relayState)).append("\"/>");
|
||||
for (String key: inputTypes.keySet()) {
|
||||
builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"").append(key).append("\"").append(" VALUE=\"").append(escapeAttribute(inputTypes.get(key))).append("\"/>");
|
||||
}
|
||||
|
||||
builder.append("<NOSCRIPT>")
|
||||
.append("<P>JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue.</P>")
|
||||
.append("<INPUT TYPE=\"SUBMIT\" VALUE=\"CONTINUE\" />")
|
||||
.append("</NOSCRIPT>")
|
||||
.append("<P>JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue.</P>")
|
||||
.append("<INPUT TYPE=\"SUBMIT\" VALUE=\"CONTINUE\" />")
|
||||
.append("</NOSCRIPT>")
|
||||
|
||||
.append("</FORM></BODY></HTML>");
|
||||
.append("</FORM></BODY></HTML>");
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
|
||||
public String base64Encoded(Document document) throws ConfigurationException, ProcessingException, IOException {
|
||||
String documentAsString = DocumentUtil.getDocumentAsString(document);
|
||||
logger.debugv("saml document: {0}", documentAsString);
|
||||
|
@ -359,7 +372,7 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
|
|||
int pos = builder.getQuery() == null? 0 : builder.getQuery().length();
|
||||
builder.queryParam(samlParameterName, base64Encoded(document));
|
||||
if (relayState != null) {
|
||||
builder.queryParam("RelayState", relayState);
|
||||
builder.queryParam(GeneralConstants.RELAY_STATE, relayState);
|
||||
}
|
||||
|
||||
if (sign) {
|
||||
|
|
|
@ -51,11 +51,20 @@ import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_
|
|||
public class SPMetadataDescriptor {
|
||||
|
||||
public static String getSPDescriptor(URI binding, URI assertionEndpoint, URI logoutEndpoint,
|
||||
boolean wantAuthnRequestsSigned, boolean wantAssertionsSigned, boolean wantAssertionsEncrypted,
|
||||
String entityId, String nameIDPolicyFormat, List<Element> signingCerts, List<Element> encryptionCerts)
|
||||
throws XMLStreamException, ProcessingException, ParserConfigurationException
|
||||
{
|
||||
return getSPDescriptor(binding, binding, assertionEndpoint, logoutEndpoint, wantAuthnRequestsSigned,
|
||||
wantAssertionsSigned, wantAssertionsEncrypted, entityId, nameIDPolicyFormat, signingCerts,
|
||||
encryptionCerts);
|
||||
}
|
||||
|
||||
public static String getSPDescriptor(URI loginBinding, URI logoutBinding, URI assertionEndpoint, URI logoutEndpoint,
|
||||
boolean wantAuthnRequestsSigned, boolean wantAssertionsSigned, boolean wantAssertionsEncrypted,
|
||||
String entityId, String nameIDPolicyFormat, List<Element> signingCerts, List<Element> encryptionCerts)
|
||||
throws XMLStreamException, ProcessingException, ParserConfigurationException
|
||||
{
|
||||
|
||||
StringWriter sw = new StringWriter();
|
||||
XMLStreamWriter writer = StaxUtil.getXMLStreamWriter(sw);
|
||||
SAMLMetadataWriter metadataWriter = new SAMLMetadataWriter(writer);
|
||||
|
@ -67,7 +76,7 @@ public class SPMetadataDescriptor {
|
|||
spSSODescriptor.setAuthnRequestsSigned(wantAuthnRequestsSigned);
|
||||
spSSODescriptor.setWantAssertionsSigned(wantAssertionsSigned);
|
||||
spSSODescriptor.addNameIDFormat(nameIDPolicyFormat);
|
||||
spSSODescriptor.addSingleLogoutService(new EndpointType(binding, logoutEndpoint));
|
||||
spSSODescriptor.addSingleLogoutService(new EndpointType(logoutBinding, logoutEndpoint));
|
||||
|
||||
if (wantAuthnRequestsSigned && signingCerts != null) {
|
||||
for (Element key: signingCerts)
|
||||
|
@ -89,7 +98,7 @@ public class SPMetadataDescriptor {
|
|||
}
|
||||
}
|
||||
|
||||
IndexedEndpointType assertionConsumerEndpoint = new IndexedEndpointType(binding, assertionEndpoint);
|
||||
IndexedEndpointType assertionConsumerEndpoint = new IndexedEndpointType(loginBinding, assertionEndpoint);
|
||||
assertionConsumerEndpoint.setIsDefault(true);
|
||||
assertionConsumerEndpoint.setIndex(1);
|
||||
spSSODescriptor.addAssertionConsumerService(assertionConsumerEndpoint);
|
||||
|
|
|
@ -177,7 +177,17 @@ public class SAML2Request {
|
|||
throw logger.nullArgumentError("InputStream");
|
||||
|
||||
Document samlDocument = DocumentUtil.getDocument(is);
|
||||
return getSAML2ObjectFromDocument(samlDocument);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Underlying SAML2Object from a document
|
||||
* @param samlDocument a Document containing a SAML2Object
|
||||
* @return a SAMLDocumentHolder
|
||||
* @throws ProcessingException
|
||||
* @throws ParsingException
|
||||
*/
|
||||
public static SAMLDocumentHolder getSAML2ObjectFromDocument(Document samlDocument) throws ProcessingException, ParsingException {
|
||||
SAMLParser samlParser = SAMLParser.getInstance();
|
||||
JAXPValidationUtil.checkSchemaValidation(samlDocument);
|
||||
SAML2Object requestType = (SAML2Object) samlParser.parse(samlDocument);
|
||||
|
|
|
@ -90,6 +90,16 @@ public class SAMLArtifactResponseParser extends SAMLStatusResponseTypeParser<Art
|
|||
target.setStatus(SAMLStatusParser.getInstance().parse(xmlEventReader));
|
||||
break;
|
||||
|
||||
case LOGOUT_REQUEST:
|
||||
SAMLSloRequestParser sloRequestParser = SAMLSloRequestParser.getInstance();
|
||||
target.setAny(sloRequestParser.parse(xmlEventReader));
|
||||
break;
|
||||
|
||||
case LOGOUT_RESPONSE:
|
||||
SAMLSloResponseParser sloResponseParser = SAMLSloResponseParser.getInstance();
|
||||
target.setAny(sloResponseParser.parse(xmlEventReader));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw LOGGER.parserUnknownTag(StaxParserUtil.getElementName(elementDetail), elementDetail.getLocation());
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import org.keycloak.dom.saml.v2.assertion.EncryptedAssertionType;
|
|||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
|
||||
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
||||
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
|
||||
import org.keycloak.dom.saml.v2.protocol.StatusDetailType;
|
||||
|
@ -40,6 +41,8 @@ import java.util.List;
|
|||
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
||||
import javax.xml.crypto.dsig.XMLSignature;
|
||||
|
||||
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_NSURI;
|
||||
|
||||
/**
|
||||
* Write a SAML Response to stream
|
||||
*
|
||||
|
@ -135,9 +138,16 @@ public class SAMLResponseWriter extends BaseWriter {
|
|||
AuthnRequestType authn = (AuthnRequestType) anyObj;
|
||||
SAMLRequestWriter requestWriter = new SAMLRequestWriter(writer);
|
||||
requestWriter.write(authn);
|
||||
} else if (anyObj instanceof LogoutRequestType) {
|
||||
LogoutRequestType logoutRequestType = (LogoutRequestType) anyObj;
|
||||
SAMLRequestWriter requestWriter = new SAMLRequestWriter(writer);
|
||||
requestWriter.write(logoutRequestType);
|
||||
} else if (anyObj instanceof ResponseType) {
|
||||
ResponseType rt = (ResponseType) anyObj;
|
||||
write(rt);
|
||||
} else if (anyObj instanceof StatusResponseType) {
|
||||
StatusResponseType rt = (StatusResponseType) anyObj;
|
||||
write(rt, new QName(PROTOCOL_NSURI.get(), JBossSAMLConstants.LOGOUT_RESPONSE.get(), "samlp"));
|
||||
}
|
||||
|
||||
StaxUtil.writeEndElement(writer);
|
||||
|
|
|
@ -52,6 +52,8 @@ public interface Errors {
|
|||
String INVALID_SAML_AUTHN_REQUEST = "invalid_authn_request";
|
||||
String INVALID_SAML_LOGOUT_REQUEST = "invalid_logout_request";
|
||||
String INVALID_SAML_LOGOUT_RESPONSE = "invalid_logout_response";
|
||||
String INVALID_SAML_ARTIFACT = "invalid_artifact";
|
||||
String INVALID_SAML_ARTIFACT_RESPONSE = "invalid_artifact_response";
|
||||
String SAML_TOKEN_NOT_FOUND = "saml_token_not_found";
|
||||
String INVALID_SIGNATURE = "invalid_signature";
|
||||
String INVALID_REGISTRATION = "invalid_registration";
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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.models;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
/**
|
||||
* Provides cache for session mapping for SAML artifacts.
|
||||
*
|
||||
* For now, it is separate provider as it's a bit different use-case than {@link ActionTokenStoreProvider}, however it may reuse some components (eg. same infinispan cache)
|
||||
*
|
||||
* @author mhajas
|
||||
*/
|
||||
public interface SamlArtifactSessionMappingStoreProvider extends Provider {
|
||||
|
||||
/**
|
||||
* Stores the given data and guarantees that data should be available in the store for at least the time specified by {@param lifespanSeconds} parameter
|
||||
*
|
||||
* @param artifact
|
||||
* @param lifespanSeconds
|
||||
* @param clientSessionModel
|
||||
*/
|
||||
void put(String artifact, int lifespanSeconds, AuthenticatedClientSessionModel clientSessionModel);
|
||||
|
||||
|
||||
/**
|
||||
* This method returns session mapping associated with the given {@param artifact}
|
||||
*
|
||||
* @param artifact
|
||||
* @return session mapping corresponding to given artifact or {@code null} if it does not exist.
|
||||
*/
|
||||
SamlArtifactSessionMappingModel get(String artifact);
|
||||
|
||||
/**
|
||||
* Removes data for the given {@param artifact} from the store
|
||||
*
|
||||
* @param artifact
|
||||
*/
|
||||
void remove(String artifact);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.models;
|
||||
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
public interface SamlArtifactSessionMappingStoreProviderFactory extends ProviderFactory<SamlArtifactSessionMappingStoreProvider> {
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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.models;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
public class SamlArtifactSessionMappingStoreSpi implements Spi {
|
||||
|
||||
public static final String NAME = "samlArtifactSessionMappingStore";
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return SamlArtifactSessionMappingStoreProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return SamlArtifactSessionMappingStoreProviderFactory.class;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package org.keycloak.protocol.saml;
|
||||
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
|
||||
/**
|
||||
* Provides a way to create and resolve artifacts for SAML Artifact binding
|
||||
*/
|
||||
public interface ArtifactResolver extends Provider {
|
||||
|
||||
/**
|
||||
* Returns client model that issued artifact
|
||||
*
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* Creates and stores an artifact
|
||||
*
|
||||
* @param clientSessionModel client session model that can be used for storing the response for artifact
|
||||
* @param entityId id of an issuer that issued the artifactResponse
|
||||
* @param artifactResponse serialized Saml ArtifactResponse that represents the response for created artifact
|
||||
* @return the artifact
|
||||
* @throws ArtifactResolverProcessingException When an error occurs during creation of the artifact.
|
||||
*/
|
||||
String buildArtifact(AuthenticatedClientSessionModel clientSessionModel, String entityId, String artifactResponse) throws ArtifactResolverProcessingException;
|
||||
|
||||
/**
|
||||
* Returns a serialized Saml ArtifactResponse corresponding to the artifact that was created by
|
||||
* {@link #buildArtifact(AuthenticatedClientSessionModel, String, String) buildArtifact}
|
||||
*
|
||||
* @param clientSessionModel client session model that can be used for obtaining the artifact response
|
||||
* @param artifact the artifact
|
||||
* @return serialized Saml ArtifactResponse corresponding to the artifact
|
||||
* @throws ArtifactResolverProcessingException When an error occurs during resolution of the artifact.
|
||||
*/
|
||||
String resolveArtifact(AuthenticatedClientSessionModel clientSessionModel, String artifact) throws ArtifactResolverProcessingException;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package org.keycloak.protocol.saml;
|
||||
|
||||
/**
|
||||
* Exception to indicate a configuration error in {@link ArtifactResolver}.
|
||||
*
|
||||
*/
|
||||
public class ArtifactResolverConfigException extends Exception {
|
||||
|
||||
public ArtifactResolverConfigException(Exception e){
|
||||
super(e);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package org.keycloak.protocol.saml;
|
||||
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
/**
|
||||
* A factory that creates {@link ArtifactResolver} instances.
|
||||
*/
|
||||
public interface ArtifactResolverFactory extends ProviderFactory<ArtifactResolver> {
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package org.keycloak.protocol.saml;
|
||||
|
||||
/**
|
||||
* Exception to indicate a processing error in {@link ArtifactResolver}.
|
||||
*
|
||||
*/
|
||||
public class ArtifactResolverProcessingException extends Exception{
|
||||
|
||||
public ArtifactResolverProcessingException(Exception e){
|
||||
super(e);
|
||||
}
|
||||
|
||||
public ArtifactResolverProcessingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ArtifactResolverProcessingException(String message, Exception e){
|
||||
super(message, e);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package org.keycloak.protocol.saml;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class ArtifactResolverSpi implements Spi {
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "saml-artifact-resolver";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return ArtifactResolver.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return ArtifactResolverFactory.class;
|
||||
}
|
||||
}
|
|
@ -29,6 +29,7 @@ org.keycloak.models.ActionTokenStoreSpi
|
|||
org.keycloak.models.CodeToTokenStoreSpi
|
||||
org.keycloak.models.OAuth2DeviceTokenStoreSpi
|
||||
org.keycloak.models.OAuth2DeviceUserCodeSpi
|
||||
org.keycloak.models.SamlArtifactSessionMappingStoreSpi
|
||||
org.keycloak.models.SingleUseTokenStoreSpi
|
||||
org.keycloak.models.TokenRevocationStoreSpi
|
||||
org.keycloak.models.UserSessionSpi
|
||||
|
@ -73,6 +74,7 @@ org.keycloak.authorization.store.StoreFactorySpi
|
|||
org.keycloak.authorization.AuthorizationSpi
|
||||
org.keycloak.models.cache.authorization.CachedStoreFactorySpi
|
||||
org.keycloak.protocol.oidc.TokenIntrospectionSpi
|
||||
org.keycloak.protocol.saml.ArtifactResolverSpi
|
||||
org.keycloak.policy.PasswordPolicySpi
|
||||
org.keycloak.policy.PasswordPolicyManagerSpi
|
||||
org.keycloak.transaction.TransactionManagerLookupSpi
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package org.keycloak.models;
|
||||
|
||||
public class SamlArtifactSessionMappingModel {
|
||||
|
||||
private final String userSessionId;
|
||||
private final String clientSessionId;
|
||||
|
||||
public SamlArtifactSessionMappingModel(String userSessionId, String clientSessionId) {
|
||||
this.userSessionId = userSessionId;
|
||||
this.clientSessionId = clientSessionId;
|
||||
}
|
||||
|
||||
public String getUserSessionId() {
|
||||
return userSessionId;
|
||||
}
|
||||
|
||||
public String getClientSessionId() {
|
||||
return clientSessionId;
|
||||
}
|
||||
}
|
|
@ -88,7 +88,8 @@ public interface UserSessionModel {
|
|||
enum State {
|
||||
LOGGED_IN,
|
||||
LOGGING_OUT,
|
||||
LOGGED_OUT
|
||||
LOGGED_OUT,
|
||||
LOGGED_OUT_UNCONFIRMED;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.broker.saml;
|
|||
import org.keycloak.broker.provider.DefaultDataMarshaller;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
|
@ -58,6 +59,10 @@ public class SAMLDataMarshaller extends DefaultDataMarshaller {
|
|||
AuthnStatementType authnStatement = (AuthnStatementType) obj;
|
||||
SAMLAssertionWriter samlWriter = new SAMLAssertionWriter(StaxUtil.getXMLStreamWriter(bos));
|
||||
samlWriter.write(authnStatement, true);
|
||||
} else if (obj instanceof ArtifactResponseType) {
|
||||
ArtifactResponseType artifactResponseType = (ArtifactResponseType) obj;
|
||||
SAMLResponseWriter samlWriter = new SAMLResponseWriter(StaxUtil.getXMLStreamWriter(bos));
|
||||
samlWriter.write(artifactResponseType);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Don't know how to serialize object of type " + obj.getClass().getName());
|
||||
}
|
||||
|
@ -77,7 +82,7 @@ public class SAMLDataMarshaller extends DefaultDataMarshaller {
|
|||
String xmlString = serialized;
|
||||
|
||||
try {
|
||||
if (clazz.equals(ResponseType.class) || clazz.equals(AssertionType.class) || clazz.equals(AuthnStatementType.class)) {
|
||||
if (clazz.equals(ResponseType.class) || clazz.equals(AssertionType.class) || clazz.equals(AuthnStatementType.class) || clazz.equals(ArtifactResponseType.class)) {
|
||||
byte[] bytes = xmlString.getBytes(GeneralConstants.SAML_CHARSET);
|
||||
InputStream is = new ByteArrayInputStream(bytes);
|
||||
Object respType = SAMLParser.getInstance().parse(is);
|
||||
|
|
|
@ -138,6 +138,7 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
|
|||
int maxPooledPerRoute = config.getInt("max-pooled-per-route", 64);
|
||||
int connectionPoolSize = config.getInt("connection-pool-size", 128);
|
||||
long connectionTTL = config.getLong("connection-ttl-millis", -1L);
|
||||
boolean reuseConnections = config.getBoolean("reuse-connections", true);
|
||||
long maxConnectionIdleTime = config.getLong("max-connection-idle-time-millis", 900000L);
|
||||
boolean disableCookies = config.getBoolean("disable-cookies", true);
|
||||
String clientKeystore = config.get("client-keystore");
|
||||
|
@ -152,6 +153,7 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
|
|||
.establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS)
|
||||
.maxPooledPerRoute(maxPooledPerRoute)
|
||||
.connectionPoolSize(connectionPoolSize)
|
||||
.reuseConnections(reuseConnections)
|
||||
.connectionTTL(connectionTTL, TimeUnit.MILLISECONDS)
|
||||
.maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS)
|
||||
.disableCookies(disableCookies)
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
|
|||
import org.apache.http.conn.ssl.SSLContexts;
|
||||
import org.apache.http.conn.ssl.StrictHostnameVerifier;
|
||||
import org.apache.http.conn.ssl.X509HostnameVerifier;
|
||||
import org.apache.http.impl.NoConnectionReuseStrategy;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
|
||||
|
@ -95,6 +96,7 @@ public class HttpClientBuilder {
|
|||
protected int connectionPoolSize = 128;
|
||||
protected int maxPooledPerRoute = 64;
|
||||
protected long connectionTTL = -1;
|
||||
protected boolean reuseConnections = true;
|
||||
protected TimeUnit connectionTTLUnit = TimeUnit.MILLISECONDS;
|
||||
protected long maxConnectionIdleTime = 900000;
|
||||
protected TimeUnit maxConnectionIdleTimeUnit = TimeUnit.MILLISECONDS;
|
||||
|
@ -140,6 +142,11 @@ public class HttpClientBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public HttpClientBuilder reuseConnections(boolean reuseConnections) {
|
||||
this.reuseConnections = reuseConnections;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpClientBuilder maxConnectionIdleTime(long maxConnectionIdleTime, TimeUnit unit) {
|
||||
this.maxConnectionIdleTime = maxConnectionIdleTime;
|
||||
this.maxConnectionIdleTimeUnit = unit;
|
||||
|
@ -289,6 +296,9 @@ public class HttpClientBuilder {
|
|||
.setMaxConnPerRoute(maxPooledPerRoute)
|
||||
.setConnectionTimeToLive(connectionTTL, connectionTTLUnit);
|
||||
|
||||
if (!reuseConnections) {
|
||||
builder.setConnectionReuseStrategy(new NoConnectionReuseStrategy());
|
||||
}
|
||||
|
||||
if (proxyMappings != null && !proxyMappings.isEmpty()) {
|
||||
builder.setRoutePlanner(new ProxyMappingsAwareRoutePlanner(proxyMappings));
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
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.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 static org.keycloak.protocol.saml.DefaultSamlArtifactResolverFactory.TYPE_CODE;
|
||||
|
||||
/**
|
||||
* ArtifactResolver for artifact-04 format.
|
||||
* Other kind of format for artifact are allowed by standard but not specified.
|
||||
* Artifact 04 is the only one specified in SAML2.0 specification.
|
||||
*/
|
||||
public class DefaultSamlArtifactResolver implements ArtifactResolver {
|
||||
|
||||
|
||||
protected static final Logger logger = Logger.getLogger(SamlService.class);
|
||||
|
||||
@Override
|
||||
public String resolveArtifact(AuthenticatedClientSessionModel clientSessionModel, String artifact) throws ArtifactResolverProcessingException {
|
||||
String artifactResponseString = clientSessionModel.getNote(GeneralConstants.SAML_ARTIFACT_KEY + "=" + artifact);
|
||||
clientSessionModel.removeNote(GeneralConstants.SAML_ARTIFACT_KEY + "=" + artifact);
|
||||
|
||||
logger.tracef("Artifact response for artifact %s, is %s", artifact, artifactResponseString);
|
||||
|
||||
if (Strings.isNullOrEmpty(artifactResponseString)) {
|
||||
throw new ArtifactResolverProcessingException("Artifact not present in ClientSession.");
|
||||
}
|
||||
|
||||
return artifactResponseString;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientModel selectSourceClient(String artifact, Stream<ClientModel> clients) throws ArtifactResolverProcessingException {
|
||||
try {
|
||||
byte[] source = extractSourceFromArtifact(artifact);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildArtifact(AuthenticatedClientSessionModel clientSessionModel, String entityId, String artifactResponse) throws ArtifactResolverProcessingException {
|
||||
String artifact = createArtifact(entityId);
|
||||
|
||||
clientSessionModel.setNote(GeneralConstants.SAML_ARTIFACT_KEY + "=" + artifact, artifactResponse);
|
||||
|
||||
return artifact;
|
||||
}
|
||||
|
||||
private void assertSupportedArtifactFormat(String artifactString) throws ArtifactResolverProcessingException {
|
||||
byte[] artifact = Base64.getDecoder().decode(artifactString);
|
||||
|
||||
if (artifact.length != 44) {
|
||||
throw new ArtifactResolverProcessingException("Artifact " + artifactString + " has a length of " + artifact.length + ". It should be 44");
|
||||
}
|
||||
if (artifact[0] != TYPE_CODE[0] || artifact[1] != TYPE_CODE[1]) {
|
||||
throw new ArtifactResolverProcessingException("Artifact " + artifactString + " does not start with 0x0004");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] extractSourceFromArtifact(String artifactString) throws ArtifactResolverProcessingException {
|
||||
assertSupportedArtifactFormat(artifactString);
|
||||
|
||||
byte[] artifact = Base64.getDecoder().decode(artifactString);
|
||||
|
||||
byte[] source = new byte[20];
|
||||
System.arraycopy(artifact, 4, source, 0, source.length);
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an artifact. Format is:
|
||||
* <p>
|
||||
* SAML_artifact := B64(TypeCode EndpointIndex RemainingArtifact)
|
||||
* <p>
|
||||
* TypeCode := 0x0004
|
||||
* EndpointIndex := Byte1Byte2
|
||||
* RemainingArtifact := SourceID MessageHandle
|
||||
* <p>
|
||||
* SourceID := 20-byte_sequence, used by the artifact receiver to determine artifact issuer
|
||||
* MessageHandle := 20-byte_sequence
|
||||
*
|
||||
* @param entityId the entity id to encode in the sourceId
|
||||
* @return an artifact
|
||||
* @throws ArtifactResolverProcessingException
|
||||
*/
|
||||
public String createArtifact(String entityId) throws ArtifactResolverProcessingException {
|
||||
try {
|
||||
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[] assertionHandle = new byte[20];
|
||||
handleGenerator.nextBytes(assertionHandle);
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
bos.write(TYPE_CODE);
|
||||
bos.write(trimmedIndex);
|
||||
bos.write(source);
|
||||
bos.write(assertionHandle);
|
||||
|
||||
byte[] artifact = bos.toByteArray();
|
||||
|
||||
return Base64.getEncoder().encodeToString(artifact);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new ArtifactResolverProcessingException("JVM does not support required cryptography algorithms: SHA-1/SHA1PRNG.", e);
|
||||
} catch (IOException e) {
|
||||
throw new ArtifactResolverProcessingException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package org.keycloak.protocol.saml;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
public class DefaultSamlArtifactResolverFactory implements ArtifactResolverFactory {
|
||||
|
||||
/** SAML 2 artifact type code (0x0004). */
|
||||
public static final byte[] TYPE_CODE = {0, 4};
|
||||
|
||||
private DefaultSamlArtifactResolver artifactResolver;
|
||||
|
||||
@Override
|
||||
public DefaultSamlArtifactResolver create(KeycloakSession session) {
|
||||
return artifactResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
// Nothing to initialize
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
artifactResolver = new DefaultSamlArtifactResolver();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// Nothing to close
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "default";
|
||||
}
|
||||
|
||||
}
|
|
@ -107,6 +107,46 @@ public class EntityDescriptorDescriptionConverter implements ClientDescriptionCo
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets from a SPSSO descriptor the artifact resolution service for a given index
|
||||
* @param sp an SPSSO descriptor
|
||||
* @param index the index of the artifact resolution service to return
|
||||
* @return the location of the artifact resolution service
|
||||
*/
|
||||
private static String getArtifactResolutionService(SPSSODescriptorType sp, int index) {
|
||||
List<IndexedEndpointType> endpoints = sp.getArtifactResolutionService();
|
||||
for (IndexedEndpointType endpoint : endpoints) {
|
||||
if (endpoint.getIndex() == index) {
|
||||
return endpoint.getLocation().toString();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to get from a SPSSO descriptor the default artifact resolution service. Or if it doesn't
|
||||
* exist, the artifact resolution service with the lowest index
|
||||
* @param sp an SPSSO descriptor
|
||||
* @return the location of the artifact resolution service
|
||||
*/
|
||||
private static String getArtifactResolutionService(SPSSODescriptorType sp) {
|
||||
List<IndexedEndpointType> endpoints = sp.getArtifactResolutionService();
|
||||
IndexedEndpointType firstEndpoint = null;
|
||||
for (IndexedEndpointType endpoint : endpoints) {
|
||||
if (endpoint.isIsDefault() != null && endpoint.isIsDefault()) {
|
||||
firstEndpoint = endpoint;
|
||||
break;
|
||||
}
|
||||
if (firstEndpoint == null || endpoint.getIndex() < firstEndpoint.getIndex()) {
|
||||
firstEndpoint = endpoint;
|
||||
}
|
||||
}
|
||||
if (firstEndpoint != null) {
|
||||
return firstEndpoint.getLocation().toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ClientRepresentation loadEntityDescriptors(InputStream is) {
|
||||
Object metadata;
|
||||
try {
|
||||
|
@ -172,6 +212,16 @@ public class EntityDescriptorDescriptionConverter implements ClientDescriptionCo
|
|||
if (assertionConsumerServicePaosBinding != null) {
|
||||
redirectUris.add(assertionConsumerServicePaosBinding);
|
||||
}
|
||||
String assertionConsumerServiceArtifactBinding = getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get());
|
||||
if (assertionConsumerServiceArtifactBinding != null) {
|
||||
attributes.put(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE, assertionConsumerServiceArtifactBinding);
|
||||
redirectUris.add(assertionConsumerServiceArtifactBinding);
|
||||
}
|
||||
String artifactResolutionService = getArtifactResolutionService(spDescriptorType);
|
||||
if (artifactResolutionService != null) {
|
||||
attributes.put(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE, artifactResolutionService);
|
||||
}
|
||||
|
||||
if (spDescriptorType.getNameIDFormat() != null) {
|
||||
for (String format : spDescriptorType.getNameIDFormat()) {
|
||||
String attribute = SamlClient.samlNameIDFormatToClientAttribute(format);
|
||||
|
|
|
@ -21,6 +21,7 @@ import org.keycloak.dom.saml.v2.metadata.EndpointType;
|
|||
import org.keycloak.dom.saml.v2.metadata.EntitiesDescriptorType;
|
||||
import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType;
|
||||
import org.keycloak.dom.saml.v2.metadata.IDPSSODescriptorType;
|
||||
import org.keycloak.dom.saml.v2.metadata.IndexedEndpointType;
|
||||
import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType;
|
||||
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
|
||||
|
||||
|
@ -38,6 +39,7 @@ import org.keycloak.saml.common.util.StaxUtil;
|
|||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING;
|
||||
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.SAML_HTTP_POST_BINDING;
|
||||
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING;
|
||||
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.SAML_SOAP_BINDING;
|
||||
|
@ -54,7 +56,7 @@ import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_
|
|||
public class IDPMetadataDescriptor {
|
||||
|
||||
public static String getIDPDescriptor(URI loginPostEndpoint, URI loginRedirectEndpoint, URI logoutEndpoint,
|
||||
String entityId, boolean wantAuthnRequestsSigned, List<Element> signingCerts)
|
||||
URI artifactResolutionService, String entityId, boolean wantAuthnRequestsSigned, List<Element> signingCerts)
|
||||
throws ProcessingException
|
||||
{
|
||||
|
||||
|
@ -76,9 +78,13 @@ public class IDPMetadataDescriptor {
|
|||
|
||||
spIDPDescriptor.addSingleLogoutService(new EndpointType(SAML_HTTP_POST_BINDING.getUri(), logoutEndpoint));
|
||||
spIDPDescriptor.addSingleLogoutService(new EndpointType(SAML_HTTP_REDIRECT_BINDING.getUri(), logoutEndpoint));
|
||||
spIDPDescriptor.addSingleLogoutService(new EndpointType(SAML_HTTP_ARTIFACT_BINDING.getUri(), logoutEndpoint));
|
||||
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_HTTP_POST_BINDING.getUri(), loginPostEndpoint));
|
||||
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_HTTP_REDIRECT_BINDING.getUri(), loginRedirectEndpoint));
|
||||
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_SOAP_BINDING.getUri(), loginPostEndpoint));
|
||||
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_HTTP_ARTIFACT_BINDING.getUri(), loginPostEndpoint));
|
||||
|
||||
spIDPDescriptor.addArtifactResolutionService(new IndexedEndpointType(SAML_SOAP_BINDING.getUri(), artifactResolutionService));
|
||||
|
||||
if (wantAuthnRequestsSigned && signingCerts != null) {
|
||||
for (Element key: signingCerts)
|
||||
|
|
|
@ -119,6 +119,14 @@ public class SamlClient extends ClientConfigResolver {
|
|||
client.setAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE, Boolean.toString(val));
|
||||
}
|
||||
|
||||
public boolean forceArtifactBinding(){
|
||||
return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING));
|
||||
}
|
||||
|
||||
public void setForceArtifactBinding(boolean val) {
|
||||
client.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, Boolean.toString(val));
|
||||
}
|
||||
|
||||
public boolean requiresRealmSignature() {
|
||||
return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE));
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ public interface SamlConfigAttributes {
|
|||
String SAML_AUTHNSTATEMENT = "saml.authnstatement";
|
||||
String SAML_ONETIMEUSE_CONDITION = "saml.onetimeuse.condition";
|
||||
String SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE = "saml_force_name_id_format";
|
||||
String SAML_ARTIFACT_BINDING = "saml.artifact.binding";
|
||||
String SAML_SERVER_SIGNATURE = "saml.server.signature";
|
||||
String SAML_SERVER_SIGNATURE_KEYINFO_EXT = "saml.server.signature.keyinfo.ext";
|
||||
String SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER = "saml.server.signature.keyinfo.xmlSigKeyInfoKeyNameTransformer";
|
||||
|
|
|
@ -23,11 +23,16 @@ import org.apache.http.client.entity.UrlEncodedFormEntity;
|
|||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.broker.saml.SAMLDataMarshaller;
|
||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||
import org.keycloak.connections.httpclient.HttpClientProvider;
|
||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
|
||||
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
|
@ -38,6 +43,7 @@ import org.keycloak.models.KeyManager;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.SamlArtifactSessionMappingStoreProvider;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
|
@ -51,6 +57,7 @@ import org.keycloak.saml.SAML2ErrorResponseBuilder;
|
|||
import org.keycloak.saml.SAML2LoginResponseBuilder;
|
||||
import org.keycloak.saml.SAML2LogoutRequestBuilder;
|
||||
import org.keycloak.saml.SAML2LogoutResponseBuilder;
|
||||
import org.keycloak.saml.SAML2NameIDBuilder;
|
||||
import org.keycloak.saml.SamlProtocolExtensionsAwareBuilder.NodeGenerator;
|
||||
import org.keycloak.saml.SignatureAlgorithm;
|
||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||
|
@ -66,11 +73,12 @@ import org.keycloak.services.managers.AuthenticationSessionManager;
|
|||
import org.keycloak.services.managers.ResourceAdminManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.sessions.CommonClientSessionModel;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.sessions.CommonClientSessionModel;
|
||||
import org.w3c.dom.Document;
|
||||
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
@ -96,14 +104,15 @@ import org.apache.http.util.EntityUtils;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class SamlProtocol implements LoginProtocol {
|
||||
protected static final Logger logger = Logger.getLogger(SamlProtocol.class);
|
||||
|
||||
public static final String ATTRIBUTE_TRUE_VALUE = "true";
|
||||
public static final String ATTRIBUTE_FALSE_VALUE = "false";
|
||||
public static final String SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE = "saml_assertion_consumer_url_post";
|
||||
public static final String SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE = "saml_assertion_consumer_url_redirect";
|
||||
public static final String SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE = "saml_artifact_binding_url";
|
||||
public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE = "saml_single_logout_service_url_post";
|
||||
public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE = "saml_single_logout_service_url_artifact";
|
||||
public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE = "saml_single_logout_service_url_redirect";
|
||||
public static final String SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE = "saml_artifact_resolution_service_url";
|
||||
public static final String LOGIN_PROTOCOL = "saml";
|
||||
public static final String SAML_BINDING = "saml_binding";
|
||||
public static final String SAML_IDP_INITIATED_LOGIN = "saml_idp_initiated_login";
|
||||
|
@ -127,6 +136,9 @@ public class SamlProtocol implements LoginProtocol {
|
|||
public static final String SAML_IDP_INITIATED_SSO_URL_NAME = "saml_idp_initiated_sso_url_name";
|
||||
public static final String SAML_LOGIN_REQUEST_FORCEAUTHN = "SAML_LOGIN_REQUEST_FORCEAUTHN";
|
||||
public static final String SAML_FORCEAUTHN_REQUIREMENT = "true";
|
||||
public static final String SAML_LOGOUT_INITIATOR_CLIENT_ID = "SAML_LOGOUT_INITIATOR_CLIENT_ID";
|
||||
|
||||
protected static final Logger logger = Logger.getLogger(SamlProtocol.class);
|
||||
|
||||
protected KeycloakSession session;
|
||||
|
||||
|
@ -138,6 +150,9 @@ public class SamlProtocol implements LoginProtocol {
|
|||
|
||||
protected EventBuilder event;
|
||||
|
||||
protected ArtifactResolver artifactResolver;
|
||||
protected SamlArtifactSessionMappingStoreProvider artifactSessionMappingStore;
|
||||
|
||||
@Override
|
||||
public SamlProtocol setSession(KeycloakSession session) {
|
||||
this.session = session;
|
||||
|
@ -168,6 +183,20 @@ public class SamlProtocol implements LoginProtocol {
|
|||
return this;
|
||||
}
|
||||
|
||||
private ArtifactResolver getArtifactResolver() {
|
||||
if (artifactResolver == null) {
|
||||
artifactResolver = session.getProvider(ArtifactResolver.class);
|
||||
}
|
||||
return artifactResolver;
|
||||
}
|
||||
|
||||
private SamlArtifactSessionMappingStoreProvider getArtifactSessionMappingStore() {
|
||||
if (artifactSessionMappingStore == null) {
|
||||
artifactSessionMappingStore = session.getProvider(SamlArtifactSessionMappingStoreProvider.class);
|
||||
}
|
||||
return artifactSessionMappingStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response sendError(AuthenticationSessionModel authSession, Error error) {
|
||||
try {
|
||||
|
@ -187,8 +216,8 @@ public class SamlProtocol implements LoginProtocol {
|
|||
}
|
||||
} else {
|
||||
return samlErrorMessage(
|
||||
authSession, new SamlClient(client), isPostBinding(authSession),
|
||||
authSession.getRedirectUri(), translateErrorToSAMLStatus(error), authSession.getClientNote(GeneralConstants.RELAY_STATE)
|
||||
authSession, new SamlClient(client), isPostBinding(authSession),
|
||||
authSession.getRedirectUri(), translateErrorToSAMLStatus(error), authSession.getClientNote(GeneralConstants.RELAY_STATE)
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
|
@ -197,8 +226,8 @@ public class SamlProtocol implements LoginProtocol {
|
|||
}
|
||||
|
||||
private Response samlErrorMessage(
|
||||
AuthenticationSessionModel authSession, SamlClient samlClient, boolean isPostBinding,
|
||||
String destination, JBossSAMLURIConstants statusDetail, String relayState) {
|
||||
AuthenticationSessionModel authSession, SamlClient samlClient, boolean isPostBinding,
|
||||
String destination, JBossSAMLURIConstants statusDetail, String relayState) {
|
||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(session).relayState(relayState);
|
||||
SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(destination).issuer(getResponseIssuer(realm)).status(statusDetail.get());
|
||||
KeyManager keyManager = session.keys();
|
||||
|
@ -312,13 +341,13 @@ public class SamlProtocol implements LoginProtocol {
|
|||
String configuredNameIdFormat = samlClient.getNameIDFormat();
|
||||
if ((nameIdFormat == null || forceFormat) && configuredNameIdFormat != null) {
|
||||
nameIdFormat = configuredNameIdFormat;
|
||||
}
|
||||
}
|
||||
if (nameIdFormat == null)
|
||||
return SAML_DEFAULT_NAMEID_FORMAT;
|
||||
return nameIdFormat;
|
||||
}
|
||||
|
||||
protected String getNameId(String nameIdFormat, CommonClientSessionModel clientSession, UserSessionModel userSession) {
|
||||
protected String getNameId(String nameIdFormat, CommonClientSessionModel clientSession, UserSessionModel userSession) {
|
||||
if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) {
|
||||
final String email = userSession.getUser().getEmail();
|
||||
if (email == null) {
|
||||
|
@ -342,11 +371,11 @@ public class SamlProtocol implements LoginProtocol {
|
|||
* Attempts to retrieve the persistent type NameId as follows:
|
||||
*
|
||||
* <ol>
|
||||
* <li>saml.persistent.name.id.for.$clientId user attribute</li>
|
||||
* <li>saml.persistent.name.id.for.* user attribute</li>
|
||||
* <li>G-$randomUuid</li>
|
||||
* <li>saml.persistent.name.id.for.$clientId user attribute</li>
|
||||
* <li>saml.persistent.name.id.for.* user attribute</li>
|
||||
* <li>G-$randomUuid</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>
|
||||
* If a randomUuid is generated, an attribute for the given saml.persistent.name.id.for.$clientId will be generated,
|
||||
* otherwise no state change will occur with respect to the user's attributes.
|
||||
*
|
||||
|
@ -389,8 +418,8 @@ public class SamlProtocol implements LoginProtocol {
|
|||
|
||||
if (nameId == null) {
|
||||
return samlErrorMessage(
|
||||
null, samlClient, isPostBinding(authSession),
|
||||
redirectUri, JBossSAMLURIConstants.STATUS_INVALID_NAMEIDPOLICY, relayState
|
||||
null, samlClient, isPostBinding(authSession),
|
||||
redirectUri, JBossSAMLURIConstants.STATUS_INVALID_NAMEIDPOLICY, relayState
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -418,7 +447,7 @@ public class SamlProtocol implements LoginProtocol {
|
|||
builder.disableAuthnStatement(true);
|
||||
}
|
||||
|
||||
builder.includeOneTimeUseCondition(samlClient.includeOneTimeUseCondition());
|
||||
builder.includeOneTimeUseCondition(samlClient.includeOneTimeUseCondition());
|
||||
|
||||
List<ProtocolMapperProcessor<SAMLAttributeStatementMapper>> attributeStatementMappers = new LinkedList<>();
|
||||
List<ProtocolMapperProcessor<SAMLLoginResponseMapper>> loginResponseMappers = new LinkedList<>();
|
||||
|
@ -441,17 +470,18 @@ public class SamlProtocol implements LoginProtocol {
|
|||
});
|
||||
|
||||
Document samlDocument = null;
|
||||
ResponseType samlModel = null;
|
||||
KeyManager keyManager = session.keys();
|
||||
KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm);
|
||||
boolean postBinding = isPostBinding(authSession);
|
||||
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
|
||||
|
||||
try {
|
||||
if ((! postBinding) && samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
|
||||
if ((!postBinding) && samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
|
||||
builder.addExtension(new KeycloakKeySamlExtensionGenerator(keyName));
|
||||
}
|
||||
|
||||
ResponseType samlModel = builder.buildModel();
|
||||
samlModel = builder.buildModel();
|
||||
final AttributeStatementType attributeStatement = populateAttributeStatements(attributeStatementMappers, session, userSession, clientSession);
|
||||
populateRoles(roleListMapper.get(), session, userSession, clientSessionCtx, attributeStatement);
|
||||
|
||||
|
@ -462,7 +492,6 @@ public class SamlProtocol implements LoginProtocol {
|
|||
}
|
||||
|
||||
samlModel = transformLoginResponse(loginResponseMappers, samlModel, session, userSession, clientSessionCtx);
|
||||
samlDocument = builder.buildDocument(samlModel);
|
||||
} catch (Exception e) {
|
||||
logger.error("failed", e);
|
||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.FAILED_TO_PROCESS_RESPONSE);
|
||||
|
@ -471,20 +500,26 @@ public class SamlProtocol implements LoginProtocol {
|
|||
JaxrsSAML2BindingBuilder bindingBuilder = new JaxrsSAML2BindingBuilder(session);
|
||||
bindingBuilder.relayState(relayState);
|
||||
|
||||
if (samlClient.requiresRealmSignature()) {
|
||||
if ("true".equals(clientSession.getNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get()))) {
|
||||
try {
|
||||
return buildArtifactAuthenticatedResponse(clientSession, redirectUri, samlModel, bindingBuilder);
|
||||
} catch (Exception e) {
|
||||
logger.error("failed", e);
|
||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.FAILED_TO_PROCESS_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
if (samlClient.requiresRealmSignature() || samlClient.requiresAssertionSignature()) {
|
||||
String canonicalization = samlClient.getCanonicalizationMethod();
|
||||
if (canonicalization != null) {
|
||||
bindingBuilder.canonicalizationMethod(canonicalization);
|
||||
}
|
||||
bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
|
||||
}
|
||||
if (samlClient.requiresAssertionSignature()) {
|
||||
String canonicalization = samlClient.getCanonicalizationMethod();
|
||||
if (canonicalization != null) {
|
||||
bindingBuilder.canonicalizationMethod(canonicalization);
|
||||
}
|
||||
bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signAssertions();
|
||||
bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate());
|
||||
|
||||
if (samlClient.requiresRealmSignature()) bindingBuilder.signDocument();
|
||||
if (samlClient.requiresAssertionSignature()) bindingBuilder.signAssertions();
|
||||
}
|
||||
|
||||
if (samlClient.requiresEncryption()) {
|
||||
PublicKey publicKey = null;
|
||||
try {
|
||||
|
@ -496,6 +531,7 @@ public class SamlProtocol implements LoginProtocol {
|
|||
bindingBuilder.encrypt(publicKey);
|
||||
}
|
||||
try {
|
||||
samlDocument = builder.buildDocument(samlModel);
|
||||
return buildAuthenticatedResponse(clientSession, redirectUri, samlDocument, bindingBuilder);
|
||||
} catch (Exception e) {
|
||||
logger.error("failed", e);
|
||||
|
@ -551,19 +587,27 @@ public class SamlProtocol implements LoginProtocol {
|
|||
roleListMapper.mapper.mapRoles(existingAttributeStatement, roleListMapper.model, session, userSession, clientSessionCtx);
|
||||
}
|
||||
|
||||
public static String getLogoutServiceUrl(KeycloakSession session, ClientModel client, String bindingType) {
|
||||
public static String getLogoutServiceUrl(KeycloakSession session, ClientModel client, String bindingType, boolean backChannelLogout) {
|
||||
String logoutServiceUrl = null;
|
||||
if (SAML_POST_BINDING.equals(bindingType)) {
|
||||
// backchannel logout doesn't support sending artifacts
|
||||
if (!backChannelLogout && useArtifactForLogout(client)) {
|
||||
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE);
|
||||
} else if (SAML_POST_BINDING.equals(bindingType)) {
|
||||
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE);
|
||||
} else {
|
||||
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE);
|
||||
}
|
||||
|
||||
if (logoutServiceUrl == null)
|
||||
logoutServiceUrl = client.getManagementUrl();
|
||||
if (logoutServiceUrl == null || logoutServiceUrl.trim().equals(""))
|
||||
return null;
|
||||
return ResourceAdminManager.resolveUri(session, client.getRootUrl(), logoutServiceUrl);
|
||||
|
||||
}
|
||||
|
||||
public static boolean useArtifactForLogout(ClientModel client) {
|
||||
return new SamlClient(client).forceArtifactBinding()
|
||||
&& client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -572,41 +616,41 @@ public class SamlProtocol implements LoginProtocol {
|
|||
SamlClient samlClient = new SamlClient(client);
|
||||
try {
|
||||
boolean postBinding = isLogoutPostBindingForClient(clientSession);
|
||||
String bindingUri = getLogoutServiceUrl(session, client, postBinding ? SAML_POST_BINDING : SAML_REDIRECT_BINDING);
|
||||
String bindingUri = getLogoutServiceUrl(session, client, postBinding ? SAML_POST_BINDING : SAML_REDIRECT_BINDING, false);
|
||||
if (bindingUri == null) {
|
||||
logger.warnf("Failed to logout client %s, skipping this client. Please configure the logout service url in the admin console for your client applications.", client.getClientId());
|
||||
return null;
|
||||
}
|
||||
|
||||
if (postBinding) {
|
||||
LogoutRequestType logoutRequest = createLogoutRequest(bindingUri, clientSession, client);
|
||||
// This is POST binding, hence KeyID is included in dsig:KeyInfo/dsig:KeyName, no need to add <samlp:Extensions> element
|
||||
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
|
||||
return binding.postBinding(SAML2Request.convert(logoutRequest)).request(bindingUri);
|
||||
} else {
|
||||
logger.debug("frontchannel redirect binding");
|
||||
NodeGenerator[] extensions;
|
||||
NodeGenerator[] extensions = new NodeGenerator[]{};
|
||||
if (!postBinding) {
|
||||
if (samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
|
||||
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
|
||||
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
|
||||
extensions = new NodeGenerator[] { new KeycloakKeySamlExtensionGenerator(keyName) };
|
||||
} else {
|
||||
extensions = new NodeGenerator[] {};
|
||||
extensions = new NodeGenerator[]{new KeycloakKeySamlExtensionGenerator(keyName)};
|
||||
}
|
||||
LogoutRequestType logoutRequest = createLogoutRequest(bindingUri, clientSession, client, extensions);
|
||||
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
|
||||
return binding.redirectBinding(SAML2Request.convert(logoutRequest)).request(bindingUri);
|
||||
}
|
||||
} catch (ConfigurationException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (ProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (ParsingException e) {
|
||||
LogoutRequestType logoutRequest = createLogoutRequest(bindingUri, clientSession, client, extensions);
|
||||
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient, "true".equals(clientSession.getNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get())));
|
||||
|
||||
//If this session uses artifact binding, send an artifact instead of the LogoutRequest
|
||||
if ("true".equals(clientSession.getNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get()))
|
||||
&& useArtifactForLogout(client)) {
|
||||
clientSession.setAction(CommonClientSessionModel.Action.LOGGING_OUT.name());
|
||||
return buildArtifactAuthenticatedResponse(clientSession, bindingUri, logoutRequest, binding);
|
||||
}
|
||||
|
||||
Document samlDocument = SAML2Request.convert(logoutRequest);
|
||||
if (postBinding) {
|
||||
// This is POST binding, hence KeyID is included in dsig:KeyInfo/dsig:KeyName, no need to add <samlp:Extensions> element
|
||||
return binding.postBinding(samlDocument).request(bindingUri);
|
||||
} else {
|
||||
logger.debug("frontchannel redirect binding");
|
||||
return binding.redirectBinding(samlDocument).request(bindingUri);
|
||||
}
|
||||
} catch (ConfigurationException | ProcessingException | IOException | ParsingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -635,11 +679,11 @@ public class SamlProtocol implements LoginProtocol {
|
|||
}
|
||||
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
|
||||
XmlKeyInfoKeyNameTransformer transformer = XmlKeyInfoKeyNameTransformer.from(
|
||||
userSession.getNote(SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER),
|
||||
SamlClient.DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER);
|
||||
userSession.getNote(SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER),
|
||||
SamlClient.DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER);
|
||||
String keyName = transformer.getKeyName(keys.getKid(), keys.getCertificate());
|
||||
binding.signatureAlgorithm(algorithm).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
|
||||
boolean addExtension = (! postBinding) && Objects.equals("true", userSession.getNote(SamlProtocol.SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO));
|
||||
boolean addExtension = (!postBinding) && Objects.equals("true", userSession.getNote(SamlProtocol.SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO));
|
||||
if (addExtension) { // Only include extension if REDIRECT binding and signing whole SAML protocol message
|
||||
builder.addExtension(new KeycloakKeySamlExtensionGenerator(keyName));
|
||||
}
|
||||
|
@ -647,7 +691,7 @@ public class SamlProtocol implements LoginProtocol {
|
|||
Response response;
|
||||
try {
|
||||
response = buildLogoutResponse(userSession, logoutBindingUri, builder, binding);
|
||||
} catch (ConfigurationException | ProcessingException | IOException e) {
|
||||
} catch (ConfigurationException | ProcessingException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (logoutBindingUri != null) {
|
||||
|
@ -666,10 +710,18 @@ public class SamlProtocol implements LoginProtocol {
|
|||
}
|
||||
|
||||
protected Response buildLogoutResponse(UserSessionModel userSession, String logoutBindingUri, SAML2LogoutResponseBuilder builder, JaxrsSAML2BindingBuilder binding) throws ConfigurationException, ProcessingException, IOException {
|
||||
|
||||
//if artifact binding is used, send an artifact instead of the LogoutResponse
|
||||
if ("true".equals(userSession.getNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get()))) {
|
||||
return buildLogoutArtifactResponse(userSession, logoutBindingUri, builder.buildModel(), binding);
|
||||
}
|
||||
|
||||
Document samlDocument = builder.buildDocument();
|
||||
|
||||
if (isLogoutPostBindingForInitiator(userSession)) {
|
||||
return binding.postBinding(builder.buildDocument()).response(logoutBindingUri);
|
||||
return binding.postBinding(samlDocument).response(logoutBindingUri);
|
||||
} else {
|
||||
return binding.redirectBinding(builder.buildDocument()).response(logoutBindingUri);
|
||||
return binding.redirectBinding(samlDocument).response(logoutBindingUri);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -677,7 +729,7 @@ public class SamlProtocol implements LoginProtocol {
|
|||
public Response backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
|
||||
ClientModel client = clientSession.getClient();
|
||||
SamlClient samlClient = new SamlClient(client);
|
||||
String logoutUrl = getLogoutServiceUrl(session, client, SAML_POST_BINDING);
|
||||
String logoutUrl = getLogoutServiceUrl(session, client, SAML_POST_BINDING, true);
|
||||
if (logoutUrl == null) {
|
||||
logger.warnf("Can't do backchannel logout. No SingleLogoutService POST Binding registered for client: %s",
|
||||
client.getClientId());
|
||||
|
@ -687,7 +739,7 @@ public class SamlProtocol implements LoginProtocol {
|
|||
String logoutRequestString = null;
|
||||
try {
|
||||
LogoutRequestType logoutRequest = createLogoutRequest(logoutUrl, clientSession, client);
|
||||
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
|
||||
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient, false);
|
||||
// This is POST binding, hence KeyID is included in dsig:KeyInfo/dsig:KeyName, no need to add <samlp:Extensions> element
|
||||
logoutRequestString = binding.postBinding(SAML2Request.convert(logoutRequest)).encoded();
|
||||
} catch (Exception e) {
|
||||
|
@ -701,8 +753,8 @@ public class SamlProtocol implements LoginProtocol {
|
|||
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
|
||||
formparams.add(new BasicNameValuePair(GeneralConstants.SAML_REQUEST_KEY, logoutRequestString));
|
||||
formparams.add(new BasicNameValuePair("BACK_CHANNEL_LOGOUT", "BACK_CHANNEL_LOGOUT")); // for Picketlink
|
||||
// todo remove
|
||||
// this
|
||||
// todo remove
|
||||
// this
|
||||
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
|
||||
HttpPost post = new HttpPost(logoutUrl);
|
||||
post.setEntity(form);
|
||||
|
@ -742,10 +794,10 @@ public class SamlProtocol implements LoginProtocol {
|
|||
logoutBuilder.addExtension(extension);
|
||||
}
|
||||
LogoutRequestType logoutRequest = logoutBuilder.createLogoutRequest();
|
||||
for (Iterator<SamlAuthenticationPreprocessor> it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext();) {
|
||||
for (Iterator<SamlAuthenticationPreprocessor> it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext(); ) {
|
||||
logoutRequest = it.next().beforeSendingLogoutRequest(logoutRequest, clientSession.getUserSession(), clientSession);
|
||||
}
|
||||
|
||||
|
||||
return logoutRequest;
|
||||
}
|
||||
|
||||
|
@ -755,9 +807,9 @@ public class SamlProtocol implements LoginProtocol {
|
|||
return Objects.equals(SamlProtocol.SAML_FORCEAUTHN_REQUIREMENT, requireReauthentication);
|
||||
}
|
||||
|
||||
private JaxrsSAML2BindingBuilder createBindingBuilder(SamlClient samlClient) {
|
||||
private JaxrsSAML2BindingBuilder createBindingBuilder(SamlClient samlClient, boolean skipRealmSignature) {
|
||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(session);
|
||||
if (samlClient.requiresRealmSignature()) {
|
||||
if (!skipRealmSignature && samlClient.requiresRealmSignature()) {
|
||||
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
|
||||
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
|
||||
binding.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
|
||||
|
@ -769,4 +821,145 @@ public class SamlProtocol implements LoginProtocol {
|
|||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method, instead of sending the actual response with the token sends
|
||||
* the artifact message via post or redirect.
|
||||
*
|
||||
* @param clientSession the current authenticated client session
|
||||
* @param redirectUri the redirect uri to the client
|
||||
* @param samlDocument a Document containing the saml Response
|
||||
* @param bindingBuilder the current JaxrsSAML2BindingBuilder configured with information for signing and encryption
|
||||
* @return A response (POSTed form or redirect) with a newly generated artifact
|
||||
* @throws ConfigurationException
|
||||
* @throws ProcessingException
|
||||
* @throws IOException
|
||||
*/
|
||||
protected Response buildArtifactAuthenticatedResponse(AuthenticatedClientSessionModel clientSession,
|
||||
String redirectUri, SAML2Object samlDocument,
|
||||
JaxrsSAML2BindingBuilder bindingBuilder)
|
||||
throws ProcessingException, ConfigurationException {
|
||||
|
||||
try {
|
||||
String artifact = buildArtifactAndStoreResponse(samlDocument, clientSession);
|
||||
String relayState = clientSession.getNote(GeneralConstants.RELAY_STATE);
|
||||
|
||||
logger.debugf("Sending artifact %s to client %s", artifact, clientSession.getClient().getClientId());
|
||||
|
||||
if (isPostBinding(clientSession)) {
|
||||
return artifactPost(redirectUri, artifact, relayState, bindingBuilder);
|
||||
} else {
|
||||
return artifactRedirect(redirectUri, artifact, relayState);
|
||||
}
|
||||
|
||||
} catch (ArtifactResolverProcessingException e) {
|
||||
throw new ProcessingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method, instead of sending the actual response with the token, sends
|
||||
* the artifact message via post or redirect. This method is only to be used for the final LogoutResponse.
|
||||
*
|
||||
* @param userSession The current user session being logged out
|
||||
* @param redirectUri the redirect uri to the client
|
||||
* @param statusResponseType a Document containing the saml Response
|
||||
* @param bindingBuilder the current JaxrsSAML2BindingBuilder configured with information for signing and encryption
|
||||
* @return A response (POSTed form or redirect) with a newly generated artifact
|
||||
* @throws ProcessingException
|
||||
* @throws IOException
|
||||
*/
|
||||
protected Response buildLogoutArtifactResponse(UserSessionModel userSession,
|
||||
String redirectUri, StatusResponseType statusResponseType,
|
||||
JaxrsSAML2BindingBuilder bindingBuilder)
|
||||
throws ProcessingException, ConfigurationException {
|
||||
|
||||
try {
|
||||
String artifact = buildArtifactAndStoreResponse(statusResponseType, userSession);
|
||||
String relayState = userSession.getNote(SAML_LOGOUT_RELAY_STATE);
|
||||
|
||||
logger.debugf("Sending artifact for LogoutResponse %s to user %s", artifact, userSession.getLoginUsername());
|
||||
|
||||
if (isLogoutPostBindingForInitiator(userSession)) {
|
||||
return artifactPost(redirectUri, artifact, relayState, bindingBuilder);
|
||||
} else {
|
||||
return artifactRedirect(redirectUri, artifact, relayState);
|
||||
}
|
||||
|
||||
} catch (ArtifactResolverProcessingException e) {
|
||||
throw new ProcessingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected String buildArtifactAndStoreResponse(SAML2Object statusResponseType, UserSessionModel userSession) throws ArtifactResolverProcessingException, ConfigurationException, ProcessingException {
|
||||
String clientIdThatInitiatedLogout = userSession.getNote(SAML_LOGOUT_INITIATOR_CLIENT_ID);
|
||||
userSession.removeNote(SAML_LOGOUT_INITIATOR_CLIENT_ID);
|
||||
|
||||
AuthenticatedClientSessionModel clientSessionModel = userSession.getAuthenticatedClientSessionByClient(clientIdThatInitiatedLogout);
|
||||
if (clientSessionModel == null) {
|
||||
throw new IllegalStateException("Initiator client id is unknown when artifact response is created");
|
||||
}
|
||||
|
||||
return buildArtifactAndStoreResponse(statusResponseType, clientSessionModel);
|
||||
}
|
||||
|
||||
protected String buildArtifactAndStoreResponse(SAML2Object saml2Object, AuthenticatedClientSessionModel clientSessionModel) throws ArtifactResolverProcessingException, ProcessingException, ConfigurationException {
|
||||
String entityId = RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString();
|
||||
ArtifactResponseType artifactResponseType = SamlProtocolUtils.buildArtifactResponse(saml2Object, SAML2NameIDBuilder.value(getResponseIssuer(realm)).build());
|
||||
|
||||
|
||||
// Create artifact and store session mapping
|
||||
SAMLDataMarshaller marshaller = new SAMLDataMarshaller();
|
||||
String artifact = getArtifactResolver().buildArtifact(clientSessionModel, entityId, marshaller.serialize(artifactResponseType));
|
||||
getArtifactSessionMappingStore().put(artifact, realm.getAccessCodeLifespan(), clientSessionModel);
|
||||
|
||||
return artifact;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an artifact through a redirect message
|
||||
*
|
||||
* @param redirectUri the redirect uri to the client
|
||||
* @param artifact the artifact to send
|
||||
* @param relayState the current relayState
|
||||
* @return a redirect Response with the artifact
|
||||
*/
|
||||
private Response artifactRedirect(String redirectUri, String artifact, String relayState) {
|
||||
KeycloakUriBuilder builder = KeycloakUriBuilder.fromUri(redirectUri)
|
||||
.replaceQuery(null)
|
||||
.queryParam(GeneralConstants.SAML_ARTIFACT_KEY, artifact);
|
||||
|
||||
if (relayState != null) {
|
||||
builder.queryParam(GeneralConstants.RELAY_STATE, relayState);
|
||||
}
|
||||
|
||||
URI uri = builder.build();
|
||||
return Response.status(302).location(uri)
|
||||
.header("Pragma", "no-cache")
|
||||
.header("Cache-Control", "no-cache, no-store").build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an artifact through a POSTed form
|
||||
*
|
||||
* @param redirectUri the redirect uri to the client
|
||||
* @param artifact the artifact to send
|
||||
* @param relayState current relayState
|
||||
* @param bindingBuilder the current JaxrsSAML2BindingBuilder configured with information for signing and encryption
|
||||
* @return a POSTed form response, with the artifact
|
||||
*/
|
||||
private Response artifactPost(String redirectUri, String artifact, String relayState, JaxrsSAML2BindingBuilder bindingBuilder) {
|
||||
Map<String, String> inputTypes = new HashMap<>();
|
||||
inputTypes.put(GeneralConstants.SAML_ARTIFACT_KEY, artifact);
|
||||
if (relayState != null) {
|
||||
inputTypes.put(GeneralConstants.RELAY_STATE, relayState);
|
||||
}
|
||||
|
||||
String str = bindingBuilder.buildHtmlForm(redirectUri, inputTypes);
|
||||
|
||||
return Response.ok(str, MediaType.TEXT_HTML_TYPE)
|
||||
.header("Pragma", "no-cache")
|
||||
.header("Cache-Control", "no-cache, no-store").build();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,21 +17,37 @@
|
|||
|
||||
package org.keycloak.protocol.saml;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.net.URI;
|
||||
import java.security.Key;
|
||||
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
|
||||
import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
|
||||
import org.keycloak.dom.saml.v2.protocol.StatusType;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.saml.SignatureAlgorithm;
|
||||
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.ParsingException;
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
import org.keycloak.saml.common.util.DocumentUtil;
|
||||
import org.keycloak.saml.common.util.StaxUtil;
|
||||
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
|
||||
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
|
||||
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
|
||||
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLResponseWriter;
|
||||
import org.keycloak.saml.processing.web.util.RedirectBindingUtil;
|
||||
import org.w3c.dom.Document;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
|
||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
||||
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
|
||||
|
@ -40,8 +56,12 @@ import org.keycloak.rotation.HardcodedKeyLocator;
|
|||
import org.keycloak.rotation.KeyLocator;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
/**
|
||||
|
@ -198,4 +218,77 @@ public class SamlProtocolUtils {
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a saml object (an object that will be part of resulting ArtifactResponse), and inserts it as the body of
|
||||
* an ArtifactResponse. The ArtifactResponse is returned as ArtifactResponseType
|
||||
*
|
||||
* @param samlObject a Saml object
|
||||
* @param issuer issuer of the resulting ArtifactResponse, should be the same as issuer of the samlObject
|
||||
* @param statusCode status code of the resulting response
|
||||
* @return An ArtifactResponse containing the saml object.
|
||||
*/
|
||||
public static ArtifactResponseType buildArtifactResponse(SAML2Object samlObject, NameIDType issuer, URI statusCode) throws ConfigurationException, ProcessingException {
|
||||
ArtifactResponseType artifactResponse = new ArtifactResponseType(IDGenerator.create("ID_"),
|
||||
XMLTimeUtil.getIssueInstant());
|
||||
|
||||
// Status
|
||||
StatusType statusType = new StatusType();
|
||||
StatusCodeType statusCodeType = new StatusCodeType();
|
||||
statusCodeType.setValue(statusCode);
|
||||
statusType.setStatusCode(statusCodeType);
|
||||
|
||||
artifactResponse.setStatus(statusType);
|
||||
artifactResponse.setIssuer(issuer);
|
||||
artifactResponse.setAny(samlObject);
|
||||
|
||||
return artifactResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a saml object (an object that will be part of resulting ArtifactResponse), and inserts it as the body of
|
||||
* an ArtifactResponse. The ArtifactResponse is returned as ArtifactResponseType
|
||||
*
|
||||
* @param samlObject a Saml object
|
||||
* @param issuer issuer of the resulting ArtifactResponse, should be the same as issuer of the samlObject
|
||||
* @return An ArtifactResponse containing the saml object.
|
||||
*/
|
||||
public static ArtifactResponseType buildArtifactResponse(SAML2Object samlObject, NameIDType issuer) throws ConfigurationException, ProcessingException {
|
||||
return buildArtifactResponse(samlObject, issuer, JBossSAMLURIConstants.STATUS_SUCCESS.getUri());
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a saml document and inserts it as a body of ArtifactResponseType
|
||||
* @param document the document
|
||||
* @return An ArtifactResponse containing the saml document.
|
||||
*/
|
||||
public static ArtifactResponseType buildArtifactResponse(Document document) throws ParsingException, ProcessingException, ConfigurationException {
|
||||
SAML2Object samlObject = SAML2Request.getSAML2ObjectFromDocument(document).getSamlObject();
|
||||
|
||||
if (samlObject instanceof StatusResponseType) {
|
||||
return buildArtifactResponse(samlObject, ((StatusResponseType)samlObject).getIssuer());
|
||||
} else if (samlObject instanceof RequestAbstractType) {
|
||||
return buildArtifactResponse(samlObject, ((RequestAbstractType)samlObject).getIssuer());
|
||||
}
|
||||
|
||||
throw new ProcessingException("SAMLObject was not StatusResponseType or LogoutRequestType");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a SAML2 ArtifactResponse into a Document
|
||||
* @param responseType an artifactResponse
|
||||
*
|
||||
* @return an artifact response converted to a Document
|
||||
*
|
||||
* @throws ParsingException
|
||||
* @throws ConfigurationException
|
||||
* @throws ProcessingException
|
||||
*/
|
||||
public static Document convert(ArtifactResponseType responseType) throws ProcessingException, ConfigurationException,
|
||||
ParsingException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
SAMLResponseWriter writer = new SAMLResponseWriter(StaxUtil.getXMLStreamWriter(bos));
|
||||
writer.write(responseType);
|
||||
return DocumentUtil.getDocument(new ByteArrayInputStream(bos.toByteArray()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,6 +61,11 @@ public class SamlRepresentationAttributes {
|
|||
return getAttributes().get(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE);
|
||||
}
|
||||
|
||||
public String getSamlArtifactBinding() {
|
||||
if (getAttributes() == null) return null;
|
||||
return getAttributes().get(SamlConfigAttributes.SAML_ARTIFACT_BINDING);
|
||||
}
|
||||
|
||||
public String getSamlServerSignature() {
|
||||
if (getAttributes() == null) return null;
|
||||
return getAttributes().get(SamlConfigAttributes.SAML_SERVER_SIGNATURE);
|
||||
|
|
|
@ -17,85 +17,134 @@
|
|||
|
||||
package org.keycloak.protocol.saml;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.jboss.resteasy.specimpl.ResteasyHttpHeaders;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
import org.keycloak.broker.saml.SAMLDataMarshaller;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.common.util.Resteasy;
|
||||
import org.keycloak.connections.httpclient.HttpClientProvider;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.crypto.KeyStatus;
|
||||
import org.keycloak.crypto.KeyUse;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||
import org.keycloak.dom.saml.v2.assertion.BaseIDAbstractType;
|
||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||
import org.keycloak.dom.saml.v2.assertion.SubjectType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ArtifactResolveType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
|
||||
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
||||
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||
import org.keycloak.dom.saml.v2.protocol.NameIDPolicyType;
|
||||
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.executors.ExecutorsProvider;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeyManager;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakTransaction;
|
||||
import org.keycloak.models.KeycloakUriInfo;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.SamlArtifactSessionMappingModel;
|
||||
import org.keycloak.models.SamlArtifactSessionMappingStoreProvider;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.AuthorizationEndpointBase;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.LoginProtocolFactory;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
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.rotation.HardcodedKeyLocator;
|
||||
import org.keycloak.rotation.KeyLocator;
|
||||
import org.keycloak.saml.BaseSAML2BindingBuilder;
|
||||
import org.keycloak.saml.SAML2LogoutResponseBuilder;
|
||||
import org.keycloak.saml.SAML2NameIDBuilder;
|
||||
import org.keycloak.saml.SAMLRequestParser;
|
||||
import org.keycloak.saml.SignatureAlgorithm;
|
||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLConstants;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
import org.keycloak.saml.common.util.DocumentUtil;
|
||||
import org.keycloak.saml.common.util.StaxUtil;
|
||||
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
|
||||
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLRequestWriter;
|
||||
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
||||
import org.keycloak.saml.processing.web.util.PostBindingUtil;
|
||||
import org.keycloak.saml.processing.web.util.RedirectBindingUtil;
|
||||
import org.keycloak.saml.validators.DestinationValidator;
|
||||
import org.keycloak.services.ErrorPage;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.services.scheduled.ScheduledTaskRunner;
|
||||
import org.keycloak.services.util.CacheControlUtil;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.sessions.CommonClientSessionModel;
|
||||
import org.keycloak.timer.ScheduledTask;
|
||||
import org.keycloak.transaction.AsyncResponseTransaction;
|
||||
import org.keycloak.utils.MediaType;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.FormParam;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.NotFoundException;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import javax.ws.rs.container.AsyncResponse;
|
||||
import javax.ws.rs.container.Suspended;
|
||||
import javax.ws.rs.core.*;
|
||||
import javax.xml.crypto.dsig.XMLSignature;
|
||||
import javax.xml.stream.XMLStreamWriter;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.crypto.KeyUse;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.rotation.HardcodedKeyLocator;
|
||||
import org.keycloak.rotation.KeyLocator;
|
||||
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
||||
import org.keycloak.saml.validators.DestinationValidator;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.xml.crypto.dsig.XMLSignature;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.NodeList;
|
||||
import static org.keycloak.common.util.StackUtil.getShortStackTrace;
|
||||
|
||||
|
||||
/**
|
||||
* Resource class for the saml connect token service
|
||||
|
@ -106,6 +155,7 @@ import org.w3c.dom.NodeList;
|
|||
public class SamlService extends AuthorizationEndpointBase {
|
||||
|
||||
protected static final Logger logger = Logger.getLogger(SamlService.class);
|
||||
public static final String ARTIFACT_RESOLUTION_SERVICE_PATH = "resolve";
|
||||
|
||||
private final DestinationValidator destinationValidator;
|
||||
|
||||
|
@ -121,7 +171,8 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
// and we want to turn it off.
|
||||
protected boolean redirectToAuthentication;
|
||||
|
||||
protected Response basicChecks(String samlRequest, String samlResponse) {
|
||||
protected Response basicChecks(String samlRequest, String samlResponse, String artifact) {
|
||||
logger.tracef("basicChecks(%s, %s, %s)%s", samlRequest, samlResponse, artifact, getShortStackTrace());
|
||||
if (!checkSsl()) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.SSL_REQUIRED);
|
||||
|
@ -133,7 +184,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.REALM_NOT_ENABLED);
|
||||
}
|
||||
|
||||
if (samlRequest == null && samlResponse == null) {
|
||||
if (samlRequest == null && samlResponse == null && artifact == null) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.SAML_TOKEN_NOT_FOUND);
|
||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||
|
@ -216,6 +267,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
|
||||
SAML2Object samlObject = documentHolder.getSamlObject();
|
||||
|
||||
if (samlObject instanceof AuthnRequestType) {
|
||||
|
@ -236,27 +288,9 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
String issuer = requestAbstractType.getIssuer() == null ? null : issuerNameId.getValue();
|
||||
ClientModel client = realm.getClientByClientId(issuer);
|
||||
|
||||
if (client == null) {
|
||||
event.client(issuer);
|
||||
event.error(Errors.CLIENT_NOT_FOUND);
|
||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.UNKNOWN_LOGIN_REQUESTER);
|
||||
}
|
||||
|
||||
if (!client.isEnabled()) {
|
||||
event.error(Errors.CLIENT_DISABLED);
|
||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.LOGIN_REQUESTER_NOT_ENABLED);
|
||||
}
|
||||
if (client.isBearerOnly()) {
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.BEARER_ONLY);
|
||||
}
|
||||
if (!client.isStandardFlowEnabled()) {
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.STANDARD_FLOW_DISABLED);
|
||||
}
|
||||
if (!isClientProtocolCorrect(client)) {
|
||||
event.error(Errors.INVALID_CLIENT);
|
||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, "Wrong client protocol.");
|
||||
Response error = checkClientValidity(client);
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
|
||||
session.getContext().setClient(client);
|
||||
|
@ -292,6 +326,79 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a received artifact message. This means finding the client based on the content of the artifact,
|
||||
* sending an ArtifactResolve, receiving an ArtifactResponse, and handling its content based on the "standard"
|
||||
* workflows.
|
||||
*
|
||||
* @param artifact the received artifact
|
||||
* @param relayState the current relay state
|
||||
* @return a Response based on the content of the ArtifactResponse's content
|
||||
*/
|
||||
protected void handleArtifact(AsyncResponse asyncResponse, String artifact, String relayState) {
|
||||
logger.tracef("Keycloak obtained artifact %s. %s", artifact, getShortStackTrace());
|
||||
//Find client
|
||||
ClientModel client;
|
||||
try {
|
||||
client = getArtifactResolver(artifact).selectSourceClient(artifact, realm.getClientsStream());
|
||||
|
||||
Response error = checkClientValidity(client);
|
||||
if (error != null) {
|
||||
asyncResponse.resume(error);
|
||||
return;
|
||||
}
|
||||
|
||||
} catch (ArtifactResolverProcessingException e) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.detail(Details.REASON, e.getMessage());
|
||||
event.error(Errors.INVALID_SAML_ARTIFACT);
|
||||
asyncResponse.resume(ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
//send artifact resolve
|
||||
Document doc = createArtifactResolve(client.getClientId(), artifact);
|
||||
BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder();
|
||||
SamlClient samlClient = new SamlClient(client);
|
||||
if (samlClient.requiresRealmSignature()) {
|
||||
KeyManager keyManager = session.keys();
|
||||
KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm);
|
||||
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
|
||||
String canonicalization = samlClient.getCanonicalizationMethod();
|
||||
if (canonicalization != null) {
|
||||
binding.canonicalizationMethod(canonicalization);
|
||||
}
|
||||
binding.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument(doc);
|
||||
}
|
||||
String clientArtifactBindingURL = client.getAttribute(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE);
|
||||
|
||||
if (clientArtifactBindingURL == null || clientArtifactBindingURL.isEmpty()) {
|
||||
throw new ConfigurationException("There is no configured artifact resolution service for the client " + client.getClientId());
|
||||
}
|
||||
|
||||
URI clientArtifactBindingURI = new URI(clientArtifactBindingURL);
|
||||
|
||||
ExecutorService executor = session.getProvider(ExecutorsProvider.class).getExecutor("saml-artifact-pool");
|
||||
|
||||
ArtifactResolutionRunnable artifactResolutionRunnable = new ArtifactResolutionRunnable(getBindingType(), asyncResponse, doc, clientArtifactBindingURI, relayState, session.getContext().getConnection());
|
||||
ScheduledTaskRunner task = new ScheduledTaskRunner(session.getKeycloakSessionFactory(), artifactResolutionRunnable);
|
||||
executor.execute(task);
|
||||
|
||||
logger.tracef("ArtifactResolutionRunnable scheduled, current transaction will be rolled back");
|
||||
// Current transaction must be ignored due to asyncResponse.
|
||||
session.getTransactionManager().rollback();
|
||||
} catch (URISyntaxException | ProcessingException | ParsingException | ConfigurationException e) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.detail(Details.REASON, e.getMessage());
|
||||
event.error(Errors.IDENTITY_PROVIDER_ERROR);
|
||||
asyncResponse.resume(ErrorPage.error(session, null, Response.Status.INTERNAL_SERVER_ERROR, Messages.UNEXPECTED_ERROR_HANDLING_REQUEST));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract String encodeSamlDocument(Document samlDocument) throws ProcessingException;
|
||||
|
||||
protected abstract void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException;
|
||||
|
||||
protected abstract boolean containsUnencryptedSignature(SAMLDocumentHolder documentHolder);
|
||||
|
@ -315,7 +422,12 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
if (redirectUri != null && ! "null".equals(redirectUri.toString())) { // "null" is for testing purposes
|
||||
redirect = RedirectUtils.verifyRedirectUri(session, redirectUri.toString(), client);
|
||||
} else {
|
||||
if (bindingType.equals(SamlProtocol.SAML_POST_BINDING)) {
|
||||
if ((requestAbstractType.getProtocolBinding() != null
|
||||
&& JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri()
|
||||
.equals(requestAbstractType.getProtocolBinding()))
|
||||
|| samlClient.forceArtifactBinding()) {
|
||||
redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE);
|
||||
} else if (bindingType.equals(SamlProtocol.SAML_POST_BINDING)) {
|
||||
redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE);
|
||||
} else {
|
||||
redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE);
|
||||
|
@ -333,6 +445,14 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
|
||||
AuthenticationSessionModel authSession = createAuthenticationSession(client, relayState);
|
||||
|
||||
// determine if artifact binding should be used to answer the login request
|
||||
if ((requestAbstractType.getProtocolBinding() != null
|
||||
&& JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri()
|
||||
.equals(requestAbstractType.getProtocolBinding()))
|
||||
|| new SamlClient(client).forceArtifactBinding()) {
|
||||
authSession.setClientNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get(), "true");
|
||||
}
|
||||
|
||||
authSession.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
|
||||
authSession.setRedirectUri(redirect);
|
||||
authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
||||
|
@ -365,15 +485,13 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
NameIDType nameID = (NameIDType) baseID;
|
||||
authSession.setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, nameID.getValue());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (null != requestAbstractType.isForceAuthn()
|
||||
&& requestAbstractType.isForceAuthn()) {
|
||||
&& requestAbstractType.isForceAuthn()) {
|
||||
authSession.setAuthNote(SamlProtocol.SAML_LOGIN_REQUEST_FORCEAUTHN, SamlProtocol.SAML_FORCEAUTHN_REQUIREMENT);
|
||||
}
|
||||
|
||||
|
||||
for(Iterator<SamlAuthenticationPreprocessor> it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext();) {
|
||||
requestAbstractType = it.next().beforeProcessingLoginRequest(requestAbstractType, authSession);
|
||||
|
@ -390,6 +508,8 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
if (requestedProtocolBinding != null) {
|
||||
if (JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get().equals(requestedProtocolBinding.toString())) {
|
||||
return SamlProtocol.SAML_POST_BINDING;
|
||||
} else if (JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get().equals(requestedProtocolBinding.toString())) {
|
||||
return getBindingType();
|
||||
} else {
|
||||
return SamlProtocol.SAML_REDIRECT_BINDING;
|
||||
}
|
||||
|
@ -418,12 +538,12 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, false);
|
||||
if (authResult != null) {
|
||||
String logoutBinding = getBindingType();
|
||||
String postBindingUri = SamlProtocol.getLogoutServiceUrl(session, client, SamlProtocol.SAML_POST_BINDING);
|
||||
String postBindingUri = SamlProtocol.getLogoutServiceUrl(session, client, SamlProtocol.SAML_POST_BINDING, false);
|
||||
if (samlClient.forcePostBinding() && postBindingUri != null && ! postBindingUri.trim().isEmpty())
|
||||
logoutBinding = SamlProtocol.SAML_POST_BINDING;
|
||||
boolean postBinding = Objects.equals(SamlProtocol.SAML_POST_BINDING, logoutBinding);
|
||||
|
||||
String bindingUri = SamlProtocol.getLogoutServiceUrl(session, client, logoutBinding);
|
||||
String bindingUri = SamlProtocol.getLogoutServiceUrl(session, client, logoutBinding, false);
|
||||
UserSessionModel userSession = authResult.getSession();
|
||||
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING_URI, bindingUri);
|
||||
if (samlClient.requiresRealmSignature()) {
|
||||
|
@ -432,6 +552,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
}
|
||||
if (relayState != null)
|
||||
userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState);
|
||||
|
||||
userSession.setNote(SamlProtocol.SAML_LOGOUT_REQUEST_ID, logoutRequest.getID());
|
||||
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING, logoutBinding);
|
||||
userSession.setNote(SamlProtocol.SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO, Boolean.toString((! postBinding) && samlClient.addExtensionsElementWithKeyInfo()));
|
||||
|
@ -442,12 +563,21 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
|
||||
if (clientSession != null) {
|
||||
clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name());
|
||||
|
||||
//artifact binding state must be attached to the user session upon logout, as authenticated session
|
||||
//no longer exists when the LogoutResponse message is sent
|
||||
if ("true".equals(clientSession.getNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get()))
|
||||
&& SamlProtocol.useArtifactForLogout(client)){
|
||||
clientSession.setAction(AuthenticationSessionModel.Action.LOGGING_OUT.name());
|
||||
userSession.setNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get(), "true");
|
||||
userSession.setNote(SamlProtocol.SAML_LOGOUT_INITIATOR_CLIENT_ID, client.getId());
|
||||
}
|
||||
}
|
||||
|
||||
for(Iterator<SamlAuthenticationPreprocessor> it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext();) {
|
||||
logoutRequest = it.next().beforeProcessingLogoutRequest(logoutRequest, userSession, clientSession);
|
||||
}
|
||||
|
||||
|
||||
logger.debug("browser Logout");
|
||||
return authManager.browserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers, null);
|
||||
} else if (logoutRequest.getSessionIndex() != null) {
|
||||
|
@ -477,9 +607,8 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
}
|
||||
|
||||
// default
|
||||
|
||||
String logoutBinding = getBindingType();
|
||||
String logoutBindingUri = SamlProtocol.getLogoutServiceUrl(session, client, logoutBinding);
|
||||
String logoutBindingUri = SamlProtocol.getLogoutServiceUrl(session, client, logoutBinding, true);
|
||||
String logoutRelayState = relayState;
|
||||
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
|
||||
builder.logoutRequestID(logoutRequest.getID());
|
||||
|
@ -532,16 +661,37 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
}
|
||||
}
|
||||
|
||||
public Response execute(String samlRequest, String samlResponse, String relayState) {
|
||||
Response response = basicChecks(samlRequest, samlResponse);
|
||||
public Response execute(String samlRequest, String samlResponse, String relayState, String artifact) {
|
||||
Response response = basicChecks(samlRequest, samlResponse, artifact);
|
||||
if (response != null)
|
||||
return response;
|
||||
|
||||
if (samlRequest != null)
|
||||
return handleSamlRequest(samlRequest, relayState);
|
||||
else
|
||||
return handleSamlResponse(samlResponse, relayState);
|
||||
}
|
||||
|
||||
public void execute(AsyncResponse asyncReponse, String samlRequest, String samlResponse, String relayState, String artifact) {
|
||||
Response response = basicChecks(samlRequest, samlResponse, artifact);
|
||||
|
||||
if (response != null){
|
||||
asyncReponse.resume(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (artifact != null) {
|
||||
handleArtifact(asyncReponse, artifact, relayState);
|
||||
return;
|
||||
}
|
||||
if (samlRequest != null) {
|
||||
asyncReponse.resume(handleSamlRequest(samlRequest, relayState));
|
||||
return;
|
||||
} else {
|
||||
asyncReponse.resume(handleSamlResponse(samlResponse, relayState));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* KEYCLOAK-12616, KEYCLOAK-12944: construct the expected destination URI using the configured base URI.
|
||||
*
|
||||
|
@ -557,6 +707,15 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
|
||||
protected class PostBindingProtocol extends BindingProtocol {
|
||||
|
||||
@Override
|
||||
protected String encodeSamlDocument(Document samlDocument) throws ProcessingException {
|
||||
try {
|
||||
return PostBindingUtil.base64Encode(DocumentUtil.asString(samlDocument));
|
||||
} catch (IOException e) {
|
||||
throw new ProcessingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException {
|
||||
SamlProtocolUtils.verifyDocumentSignature(client, documentHolder.getSamlDocument());
|
||||
|
@ -588,6 +747,15 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
|
||||
protected class RedirectBindingProtocol extends BindingProtocol {
|
||||
|
||||
@Override
|
||||
protected String encodeSamlDocument(Document samlDocument) throws ProcessingException {
|
||||
try {
|
||||
return RedirectBindingUtil.deflateBase64Encode(DocumentUtil.asString(samlDocument).getBytes(GeneralConstants.SAML_CHARSET_NAME));
|
||||
} catch (IOException e) {
|
||||
throw new ProcessingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException {
|
||||
PublicKey publicKey = SamlProtocolUtils.getSignatureValidationKey(client);
|
||||
|
@ -629,13 +797,22 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
return handleBrowserAuthenticationRequest(authSession, samlProtocol, isPassive, redirectToAuthentication);
|
||||
}
|
||||
|
||||
public RedirectBindingProtocol newRedirectBindingProtocol() {
|
||||
return new RedirectBindingProtocol();
|
||||
}
|
||||
|
||||
public PostBindingProtocol newPostBindingProtocol() {
|
||||
return new PostBindingProtocol();
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
@GET
|
||||
public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @QueryParam(GeneralConstants.RELAY_STATE) String relayState) {
|
||||
public void redirectBinding(@Suspended AsyncResponse asyncResponse, @QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @QueryParam(GeneralConstants.RELAY_STATE) String relayState, @QueryParam(GeneralConstants.SAML_ARTIFACT_KEY) String artifact) {
|
||||
logger.debug("SAML GET");
|
||||
CacheControlUtil.noBackButtonCacheControlHeader();
|
||||
return new RedirectBindingProtocol().execute(samlRequest, samlResponse, relayState);
|
||||
|
||||
new RedirectBindingProtocol().execute(asyncResponse, samlRequest, samlResponse, relayState, artifact);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -643,14 +820,14 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
@POST
|
||||
@NoCache
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @FormParam(GeneralConstants.RELAY_STATE) String relayState) {
|
||||
public void postBinding(@Suspended AsyncResponse asyncResponse, @FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @FormParam(GeneralConstants.RELAY_STATE) String relayState, @FormParam(GeneralConstants.SAML_ARTIFACT_KEY) String artifact) {
|
||||
logger.debug("SAML POST");
|
||||
PostBindingProtocol postBindingProtocol = new PostBindingProtocol();
|
||||
// this is to support back button on browser
|
||||
// if true, we redirect to authenticate URL otherwise back button behavior has bad side effects
|
||||
// and we want to turn it off.
|
||||
postBindingProtocol.redirectToAuthentication = true;
|
||||
return postBindingProtocol.execute(samlRequest, samlResponse, relayState);
|
||||
postBindingProtocol.execute(asyncResponse, samlRequest, samlResponse, relayState, artifact);
|
||||
}
|
||||
|
||||
@GET
|
||||
|
@ -680,6 +857,8 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL),
|
||||
RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL),
|
||||
RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL),
|
||||
RealmsResource.protocolUrl(uriInfo).path(SamlService.ARTIFACT_RESOLUTION_SERVICE_PATH)
|
||||
.build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL),
|
||||
RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString(),
|
||||
true,
|
||||
signingKeys);
|
||||
|
@ -703,6 +882,37 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
return false;
|
||||
}
|
||||
|
||||
private Response checkClientValidity(ClientModel client) {
|
||||
if (client == null) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.detail(Details.REASON, "Cannot_match_source_hash");
|
||||
event.error(Errors.CLIENT_NOT_FOUND);
|
||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||
}
|
||||
if (!client.isEnabled()) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.CLIENT_DISABLED);
|
||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.LOGIN_REQUESTER_NOT_ENABLED);
|
||||
}
|
||||
if (client.isBearerOnly()) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.BEARER_ONLY);
|
||||
}
|
||||
if (!client.isStandardFlowEnabled()) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.STANDARD_FLOW_DISABLED);
|
||||
}
|
||||
if (!isClientProtocolCorrect(client)) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.error(Errors.INVALID_CLIENT);
|
||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, "Wrong client protocol.");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("clients/{client}")
|
||||
@Produces(MediaType.TEXT_HTML_UTF_8)
|
||||
|
@ -802,7 +1012,62 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
return authSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles SOAP messages. Chooses the correct response path depending on whether the message is of type ECP or Artifact
|
||||
* @param inputStream the data of the request.
|
||||
* @return The response to the SOAP message
|
||||
*/
|
||||
@POST
|
||||
@Path(ARTIFACT_RESOLUTION_SERVICE_PATH)
|
||||
@NoCache
|
||||
@Consumes({"application/soap+xml", MediaType.TEXT_XML})
|
||||
public Response artifactResolutionService(InputStream inputStream) {
|
||||
Document soapBodyContents = Soap.extractSoapMessage(inputStream);
|
||||
ArtifactResolveType artifactResolveType = null;
|
||||
SAMLDocumentHolder samlDocumentHolder = null;
|
||||
try {
|
||||
samlDocumentHolder = SAML2Request.getSAML2ObjectFromDocument(soapBodyContents);
|
||||
if (samlDocumentHolder.getSamlObject() instanceof ArtifactResolveType) {
|
||||
logger.debug("Received artifact resolve message");
|
||||
artifactResolveType = (ArtifactResolveType)samlDocumentHolder.getSamlObject();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.errorf("Artifact resolution endpoint obtained request that contained no " +
|
||||
"ArtifactResolve message: %s", DocumentUtil.asString(soapBodyContents));
|
||||
return Soap.createFault().reason("").detail("").build();
|
||||
}
|
||||
|
||||
if (artifactResolveType == null) {
|
||||
logger.errorf("Artifact resolution endpoint obtained request that contained no " +
|
||||
"ArtifactResolve message: %s", DocumentUtil.asString(soapBodyContents));
|
||||
return Soap.createFault().reason("").detail("").build();
|
||||
}
|
||||
|
||||
try {
|
||||
return artifactResolve(artifactResolveType, samlDocumentHolder);
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
return emptyArtifactResponseMessage(artifactResolveType, null, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.getUri());
|
||||
} catch (ConfigurationException | ProcessingException configurationException) {
|
||||
String reason = "An error occurred while trying to return the artifactResponse";
|
||||
String detail = e.getMessage();
|
||||
|
||||
if (detail == null) {
|
||||
detail = "";
|
||||
}
|
||||
|
||||
logger.errorf("Failure during ArtifactResolve reason: %s, detail: %s", reason, detail);
|
||||
return Soap.createFault().reason(reason).detail(detail).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles SOAP messages. Chooses the correct response path depending on whether the message is of type ECP
|
||||
* @param inputStream the data of the request.
|
||||
* @return The response to the SOAP message
|
||||
*/
|
||||
@POST
|
||||
@NoCache
|
||||
@Consumes({"application/soap+xml",MediaType.TEXT_XML})
|
||||
|
@ -813,4 +1078,373 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
|
||||
return bindingService.authenticate(inputStream);
|
||||
}
|
||||
|
||||
private ClientModel getAndCheckClientModel(String clientSessionId, String clientId) throws ProcessingException {
|
||||
ClientModel client = session.clients().getClientById(realm, clientSessionId);
|
||||
|
||||
if (client == null) {
|
||||
throw new ProcessingException(Errors.CLIENT_NOT_FOUND);
|
||||
}
|
||||
if (!client.isEnabled()) {
|
||||
throw new ProcessingException(Errors.CLIENT_DISABLED);
|
||||
}
|
||||
if (client.isBearerOnly()) {
|
||||
throw new ProcessingException(Errors.NOT_ALLOWED);
|
||||
}
|
||||
if (!client.isStandardFlowEnabled()) {
|
||||
throw new ProcessingException(Errors.NOT_ALLOWED);
|
||||
}
|
||||
if (!client.getClientId().equals(clientId)) {
|
||||
logger.errorf("Resolve message with wrong issuer. Artifact was issued for client %s, " +
|
||||
"however ArtifactResolveMessage came from client %s.", client.getClientId(), clientId);
|
||||
throw new ProcessingException(Errors.INVALID_SAML_ARTIFACT);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
|
||||
private SamlArtifactSessionMappingStoreProvider getArtifactSessionMappingStore() {
|
||||
return session.getProvider(SamlArtifactSessionMappingStoreProvider.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an artifact resolve message and returns the artifact response, if the artifact is found belonging to a session
|
||||
* of the issuer.
|
||||
* @param artifactResolveMessage The artifact resolve message sent by the client
|
||||
* @param artifactResolveHolder the document containing the artifact resolve message sent by the client
|
||||
* @return a Response containing the SOAP message with the ArifactResponse
|
||||
* @throws ParsingException
|
||||
* @throws ConfigurationException
|
||||
* @throws ProcessingException
|
||||
*/
|
||||
public Response artifactResolve(ArtifactResolveType artifactResolveMessage, SAMLDocumentHolder artifactResolveHolder) throws ParsingException, ConfigurationException, ProcessingException {
|
||||
logger.debug("Received artifactResolve message for artifact " + artifactResolveMessage.getArtifact() + "\n" +
|
||||
"Message: \n" + DocumentUtil.getDocumentAsString(artifactResolveHolder.getSamlDocument()));
|
||||
|
||||
String artifact = artifactResolveMessage.getArtifact(); // Artifact from resolve request
|
||||
if (artifact == null) {
|
||||
logger.errorf("Artifact to resolve was null");
|
||||
return emptyArtifactResponseMessage(artifactResolveMessage, null, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.getUri());
|
||||
}
|
||||
|
||||
ArtifactResolver artifactResolver = getArtifactResolver(artifact);
|
||||
|
||||
if (artifactResolver == null) {
|
||||
logger.errorf("Cannot find ArtifactResolver for artifact %s", artifact);
|
||||
return emptyArtifactResponseMessage(artifactResolveMessage, null, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.getUri());
|
||||
}
|
||||
|
||||
// Obtain details of session that issued artifact and check if it corresponds to issuer of Resolve message
|
||||
SamlArtifactSessionMappingModel sessionMapping = getArtifactSessionMappingStore().get(artifact);
|
||||
|
||||
if (sessionMapping == null) {
|
||||
logger.errorf("No data stored for artifact %s", artifact);
|
||||
return emptyArtifactResponseMessage(artifactResolveMessage, null);
|
||||
}
|
||||
|
||||
UserSessionModel userSessionModel = session.sessions().getUserSession(realm, sessionMapping.getUserSessionId());
|
||||
if (userSessionModel == null) {
|
||||
logger.errorf("UserSession with id: %s, that corresponds to artifact: %s does not exist.", sessionMapping.getUserSessionId(), artifact);
|
||||
return emptyArtifactResponseMessage(artifactResolveMessage, null);
|
||||
}
|
||||
|
||||
AuthenticatedClientSessionModel clientSessionModel = userSessionModel.getAuthenticatedClientSessions().get(sessionMapping.getClientSessionId());
|
||||
if (clientSessionModel == null) {
|
||||
logger.errorf("ClientSession with id: %s, that corresponds to artifact: %s and UserSession: %s does not exist.", sessionMapping.getClientSessionId(), artifact, sessionMapping.getUserSessionId());
|
||||
return emptyArtifactResponseMessage(artifactResolveMessage, null);
|
||||
}
|
||||
|
||||
ClientModel clientModel = getAndCheckClientModel(sessionMapping.getClientSessionId(), artifactResolveMessage.getIssuer().getValue());
|
||||
SamlClient samlClient = new SamlClient(clientModel);
|
||||
|
||||
// Check signature within ArtifactResolve request if client requires it
|
||||
if (samlClient.requiresClientSignature()) {
|
||||
try {
|
||||
SamlProtocolUtils.verifyDocumentSignature(clientModel, artifactResolveHolder.getSamlDocument());
|
||||
} catch (VerificationException e) {
|
||||
SamlService.logger.error("request validation failed", e);
|
||||
return emptyArtifactResponseMessage(artifactResolveMessage, clientModel);
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain artifactResponse from clientSessionModel
|
||||
String artifactResponseString;
|
||||
try {
|
||||
artifactResponseString = artifactResolver.resolveArtifact(clientSessionModel, artifact);
|
||||
} catch (ArtifactResolverProcessingException e) {
|
||||
logger.errorf(e, "Failed to resolve artifact: %s.", artifact);
|
||||
return emptyArtifactResponseMessage(artifactResolveMessage, clientModel);
|
||||
}
|
||||
|
||||
// Artifact is successfully resolved, we can remove session mapping from storage
|
||||
getArtifactSessionMappingStore().remove(artifact);
|
||||
|
||||
Document artifactResponseDocument = null;
|
||||
ArtifactResponseType artifactResponseType = null;
|
||||
try {
|
||||
SAMLDataMarshaller marshaller = new SAMLDataMarshaller();
|
||||
artifactResponseType = marshaller.deserialize(artifactResponseString, ArtifactResponseType.class);
|
||||
artifactResponseDocument = SamlProtocolUtils.convert(artifactResponseType);
|
||||
} catch (ParsingException | ConfigurationException | ProcessingException e) {
|
||||
logger.errorf(e,"Failed to obtain document from ArtifactResponseString: %s.", artifactResponseString);
|
||||
return emptyArtifactResponseMessage(artifactResolveMessage, clientModel);
|
||||
}
|
||||
|
||||
// If clientSession is in LOGGING_OUT action, now we can move it to LOGGED_OUT
|
||||
if (CommonClientSessionModel.Action.LOGGING_OUT.name().equals(clientSessionModel.getAction())) {
|
||||
clientSessionModel.setAction(CommonClientSessionModel.Action.LOGGED_OUT.name());
|
||||
|
||||
// If Keycloak sent LogoutResponse we need to also remove UserSession
|
||||
if (artifactResponseType.getAny() instanceof StatusResponseType
|
||||
&& artifactResponseString.contains(JBossSAMLConstants.LOGOUT_RESPONSE.get())) {
|
||||
if (!UserSessionModel.State.LOGGED_OUT_UNCONFIRMED.equals(userSessionModel.getState())) {
|
||||
logger.warnf("Keycloak issued LogoutResponse for clientSession %s, however user session %s was not in LOGGED_OUT_UNCONFIRMED state.",
|
||||
clientSessionModel.getId(), userSessionModel.getId());
|
||||
}
|
||||
AuthenticationManager.finishUnconfirmedUserSession(session, realm, userSessionModel);
|
||||
}
|
||||
}
|
||||
|
||||
return artifactResponseMessage(artifactResolveMessage, artifactResponseDocument, clientModel);
|
||||
}
|
||||
|
||||
private Response emptyArtifactResponseMessage(ArtifactResolveType artifactResolveMessage, ClientModel clientModel) throws ProcessingException, ConfigurationException {
|
||||
return emptyArtifactResponseMessage(artifactResolveMessage, clientModel, JBossSAMLURIConstants.STATUS_SUCCESS.getUri());
|
||||
}
|
||||
|
||||
private Response emptyArtifactResponseMessage(ArtifactResolveType artifactResolveMessage, ClientModel clientModel, URI responseStatusCode) throws ProcessingException, ConfigurationException {
|
||||
ArtifactResponseType artifactResponse = SamlProtocolUtils.buildArtifactResponse(null, SAML2NameIDBuilder.value(
|
||||
RealmsResource.realmBaseUrl(session.getContext().getUri()).build(realm.getName()).toString()).build(), responseStatusCode);
|
||||
|
||||
Document artifactResponseDocument;
|
||||
try {
|
||||
artifactResponseDocument = SamlProtocolUtils.convert(artifactResponse);
|
||||
} catch (ParsingException | ConfigurationException | ProcessingException e) {
|
||||
logger.errorf("Failed to obtain document from ArtifactResponse: %s.", artifactResponse);
|
||||
throw new ProcessingException(Errors.INVALID_SAML_ARTIFACT_RESPONSE, e);
|
||||
}
|
||||
|
||||
return artifactResponseMessage(artifactResolveMessage, artifactResponseDocument, clientModel);
|
||||
}
|
||||
|
||||
private Response artifactResponseMessage(ArtifactResolveType artifactResolveMessage, Document artifactResponseDocument, ClientModel clientModel) throws ProcessingException, ConfigurationException {
|
||||
// Add "inResponseTo" to artifactResponse
|
||||
if (artifactResolveMessage.getID() != null && !artifactResolveMessage.getID().trim().isEmpty()){
|
||||
Element artifactResponseElement = artifactResponseDocument.getDocumentElement();
|
||||
artifactResponseElement.setAttribute("InResponseTo", artifactResolveMessage.getID());
|
||||
}
|
||||
JaxrsSAML2BindingBuilder bindingBuilder = new JaxrsSAML2BindingBuilder(session);
|
||||
|
||||
if (clientModel != null) {
|
||||
SamlClient samlClient = new SamlClient(clientModel);
|
||||
|
||||
// Sign document/assertion if necessary, necessary to do this here, as the "inResponseTo" can only be set at this point
|
||||
if (samlClient.requiresRealmSignature() || samlClient.requiresAssertionSignature()) {
|
||||
KeyManager keyManager = session.keys();
|
||||
KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm);
|
||||
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
|
||||
String canonicalization = samlClient.getCanonicalizationMethod();
|
||||
if (canonicalization != null) {
|
||||
bindingBuilder.canonicalizationMethod(canonicalization);
|
||||
}
|
||||
bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate());
|
||||
|
||||
if (samlClient.requiresRealmSignature()) bindingBuilder.signDocument();
|
||||
if (samlClient.requiresAssertionSignature()) bindingBuilder.signAssertions();
|
||||
}
|
||||
|
||||
// Encrypt assertion if client requires it
|
||||
if (samlClient.requiresEncryption()) {
|
||||
PublicKey publicKey = null;
|
||||
try {
|
||||
publicKey = SamlProtocolUtils.getEncryptionKey(clientModel);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to obtain encryption key for client", e);
|
||||
return emptyArtifactResponseMessage(artifactResolveMessage, null);
|
||||
}
|
||||
bindingBuilder.encrypt(publicKey);
|
||||
}
|
||||
}
|
||||
|
||||
bindingBuilder.postBinding(artifactResponseDocument);
|
||||
|
||||
Soap.SoapMessageBuilder messageBuilder = Soap.createMessage();
|
||||
messageBuilder.addToBody(artifactResponseDocument);
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
String artifactResponse = DocumentUtil.asString(artifactResponseDocument);
|
||||
logger.debugf("Sending artifactResponse message for artifact %s. Message: \n %s", artifactResolveMessage.getArtifact(), artifactResponse);
|
||||
}
|
||||
|
||||
return messageBuilder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an ArtifactResolve document with the given issuer and artifact
|
||||
* @param issuer the value to set as "issuer"
|
||||
* @param artifact the value to set as "artifact"
|
||||
* @return the Document of the created ArtifactResolve message
|
||||
* @throws ProcessingException
|
||||
* @throws ParsingException
|
||||
* @throws ConfigurationException
|
||||
*/
|
||||
private Document createArtifactResolve(String issuer, String artifact) throws ProcessingException, ParsingException, ConfigurationException {
|
||||
ArtifactResolveType artifactResolve = new ArtifactResolveType(IDGenerator.create("ID_"),
|
||||
XMLTimeUtil.getIssueInstant());
|
||||
NameIDType nameIDType = new NameIDType();
|
||||
nameIDType.setValue(issuer);
|
||||
artifactResolve.setIssuer(nameIDType);
|
||||
artifactResolve.setArtifact(artifact);
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
XMLStreamWriter xmlStreamWriter = StaxUtil.getXMLStreamWriter(bos);
|
||||
new SAMLRequestWriter(xmlStreamWriter).write(artifactResolve);
|
||||
return DocumentUtil.getDocument(new ByteArrayInputStream(bos.toByteArray()));
|
||||
}
|
||||
|
||||
private ArtifactResolver getArtifactResolver(String artifact) {
|
||||
ArtifactResolver artifactResolver = session.getProvider(ArtifactResolver.class, ArtifactBindingUtils.artifactToResolverProviderId(artifact));
|
||||
return artifactResolver != null ? artifactResolver : session.getProvider(ArtifactResolver.class);
|
||||
}
|
||||
|
||||
private class ArtifactResolutionRunnable implements ScheduledTask{
|
||||
|
||||
private AsyncResponse asyncResponse;
|
||||
private URI clientArtifactBindingURI;
|
||||
private String relayState;
|
||||
private Document doc;
|
||||
private UriInfo uri;
|
||||
private String realmId;
|
||||
private HttpHeaders httpHeaders;
|
||||
private ClientConnection connection;
|
||||
private org.jboss.resteasy.spi.HttpResponse response;
|
||||
private HttpRequest request;
|
||||
private String bindingType;
|
||||
|
||||
public ArtifactResolutionRunnable(String bindingType, AsyncResponse asyncResponse, Document doc, URI clientArtifactBindingURI, String relayState, ClientConnection connection){
|
||||
this.asyncResponse = asyncResponse;
|
||||
this.doc = doc;
|
||||
this.clientArtifactBindingURI = clientArtifactBindingURI;
|
||||
this.relayState = relayState;
|
||||
this.uri = session.getContext().getUri();
|
||||
this.realmId = realm.getId();
|
||||
this.httpHeaders = new ResteasyHttpHeaders(headers.getRequestHeaders());
|
||||
this.connection = connection;
|
||||
this.response = Resteasy.getContextData(org.jboss.resteasy.spi.HttpResponse.class);
|
||||
this.request = Resteasy.getContextData(HttpRequest.class);
|
||||
this.bindingType = bindingType;
|
||||
}
|
||||
|
||||
|
||||
public void run(KeycloakSession session){
|
||||
// Initialize context
|
||||
Resteasy.pushContext(UriInfo.class, uri);
|
||||
|
||||
KeycloakTransaction tx = session.getTransactionManager();
|
||||
Resteasy.pushContext(KeycloakTransaction.class, tx);
|
||||
|
||||
Resteasy.pushContext(KeycloakSession.class, session);
|
||||
Resteasy.pushContext(HttpHeaders.class, httpHeaders);
|
||||
Resteasy.pushContext(org.jboss.resteasy.spi.HttpResponse.class, response);
|
||||
Resteasy.pushContext(HttpRequest.class, request);
|
||||
|
||||
Resteasy.pushContext(ClientConnection.class, connection);
|
||||
|
||||
RealmManager realmManager = new RealmManager(session);
|
||||
RealmModel realm = realmManager.getRealmByName(realmId);
|
||||
if (realm == null) {
|
||||
throw new NotFoundException("Realm does not exist");
|
||||
}
|
||||
session.getContext().setRealm(realm);
|
||||
|
||||
EventBuilder event = new EventBuilder(realm, session, clientConnection);
|
||||
|
||||
// Call Artifact Resolution Service
|
||||
HttpClientProvider httpClientProvider = session.getProvider(HttpClientProvider.class);
|
||||
CloseableHttpClient httpClient = httpClientProvider.getHttpClient();
|
||||
HttpPost httpPost = Soap.createMessage().addToBody(doc).buildHttpPost(clientArtifactBindingURI);
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Resolving artifact %s", DocumentUtil.asString(doc));
|
||||
}
|
||||
|
||||
try (CloseableHttpResponse result = httpClient.execute(httpPost)) {
|
||||
try {
|
||||
if (result.getStatusLine().getStatusCode() != Response.Status.OK.getStatusCode()) {
|
||||
throw new ProcessingException(String.format("Artifact resolution failed with status: %d", result.getStatusLine().getStatusCode()));
|
||||
}
|
||||
|
||||
Document soapBodyContents = Soap.extractSoapMessage(result.getEntity().getContent());
|
||||
SAMLDocumentHolder samlDoc = SAML2Request.getSAML2ObjectFromDocument(soapBodyContents);
|
||||
if (!(samlDoc.getSamlObject() instanceof ArtifactResponseType)) {
|
||||
throw new ProcessingException("Message received from ArtifactResolveService is not an ArtifactResponseMessage");
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Resolved object: %s" + DocumentUtil.asString(samlDoc.getSamlDocument()));
|
||||
}
|
||||
|
||||
ArtifactResponseType art = (ArtifactResponseType) samlDoc.getSamlObject();
|
||||
|
||||
if (art.getAny() == null) {
|
||||
AsyncResponseTransaction.finishAsyncResponseInTransaction(session, asyncResponse,
|
||||
ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.ARTIFACT_RESOLUTION_SERVICE_INVALID_RESPONSE));
|
||||
return;
|
||||
}
|
||||
|
||||
LoginProtocolFactory factory = (LoginProtocolFactory) session.getKeycloakSessionFactory().getProviderFactory(LoginProtocol.class, "saml");
|
||||
if (factory == null) {
|
||||
logger.debugf("protocol %s not found", "saml");
|
||||
throw new NotFoundException("Protocol not found");
|
||||
}
|
||||
|
||||
SamlService endpoint = (SamlService) factory.createProtocolEndpoint(realm, event);
|
||||
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
|
||||
BindingProtocol protocol;
|
||||
if (SamlProtocol.SAML_POST_BINDING.equals(bindingType)) {
|
||||
protocol = endpoint.newPostBindingProtocol();
|
||||
} else if (SamlProtocol.SAML_REDIRECT_BINDING.equals(bindingType)) {
|
||||
protocol = endpoint.newRedirectBindingProtocol();
|
||||
} else {
|
||||
throw new ConfigurationException("Invalid binding protocol: " + bindingType);
|
||||
}
|
||||
|
||||
if (art.getAny() instanceof ResponseType) {
|
||||
Document clientMessage = SAML2Request.convert((ResponseType) art.getAny());
|
||||
String response = protocol.encodeSamlDocument(clientMessage);
|
||||
|
||||
AsyncResponseTransaction.finishAsyncResponseInTransaction(session, asyncResponse,
|
||||
protocol.handleSamlResponse(response, relayState));
|
||||
} else if (art.getAny() instanceof RequestAbstractType) {
|
||||
Document clientMessage = SAML2Request.convert((RequestAbstractType) art.getAny());
|
||||
String request = protocol.encodeSamlDocument(clientMessage);
|
||||
AsyncResponseTransaction.finishAsyncResponseInTransaction(session, asyncResponse,
|
||||
protocol.handleSamlRequest(request, relayState));
|
||||
} else {
|
||||
throw new ProcessingException("Cannot recognise message contained in ArtifactResponse");
|
||||
}
|
||||
|
||||
} finally {
|
||||
EntityUtils.consumeQuietly(result.getEntity());
|
||||
}
|
||||
|
||||
} catch (IOException | ProcessingException | ParsingException e) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.detail(Details.REASON, e.getMessage());
|
||||
event.error(Errors.IDENTITY_PROVIDER_ERROR);
|
||||
|
||||
AsyncResponseTransaction.finishAsyncResponseInTransaction(session, asyncResponse,
|
||||
ErrorPage.error(session, null, Response.Status.INTERNAL_SERVER_ERROR, Messages.ARTIFACT_RESOLUTION_SERVICE_INVALID_RESPONSE));
|
||||
} catch(ConfigurationException e) {
|
||||
event.event(EventType.LOGIN);
|
||||
event.detail(Details.REASON, e.getMessage());
|
||||
event.error(Errors.IDENTITY_PROVIDER_ERROR);
|
||||
AsyncResponseTransaction.finishAsyncResponseInTransaction(session, asyncResponse,
|
||||
ErrorPage.error(session, null, Response.Status.INTERNAL_SERVER_ERROR, Messages.UNEXPECTED_ERROR_HANDLING_REQUEST));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -54,25 +54,43 @@ public class SamlSPDescriptorClientInstallation implements ClientInstallationPro
|
|||
SamlClient samlClient = new SamlClient(client);
|
||||
String assertionUrl;
|
||||
String logoutUrl;
|
||||
URI binding;
|
||||
URI loginBinding;
|
||||
URI logoutBinding = null;
|
||||
|
||||
if (samlClient.forcePostBinding()) {
|
||||
assertionUrl = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE);
|
||||
logoutUrl = client.getAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE);
|
||||
binding = JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.getUri();
|
||||
loginBinding = JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.getUri();
|
||||
} else { //redirect binding
|
||||
assertionUrl = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE);
|
||||
logoutUrl = client.getAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE);
|
||||
binding = JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri();
|
||||
loginBinding = JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri();
|
||||
}
|
||||
|
||||
if (samlClient.forceArtifactBinding()) {
|
||||
if (client.getAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE) != null) {
|
||||
logoutBinding = JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri();
|
||||
logoutUrl = client.getAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE);
|
||||
} else {
|
||||
logoutBinding = loginBinding;
|
||||
}
|
||||
|
||||
assertionUrl = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE);
|
||||
loginBinding = JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri();
|
||||
|
||||
}
|
||||
|
||||
if (assertionUrl == null || assertionUrl.trim().isEmpty()) assertionUrl = client.getManagementUrl();
|
||||
if (assertionUrl == null || assertionUrl.trim().isEmpty()) assertionUrl = FALLBACK_ERROR_URL_STRING;
|
||||
if (logoutUrl == null || logoutUrl.trim().isEmpty()) logoutUrl = client.getManagementUrl();
|
||||
if (logoutUrl == null || logoutUrl.trim().isEmpty()) logoutUrl = FALLBACK_ERROR_URL_STRING;
|
||||
if (logoutBinding == null) logoutBinding = loginBinding;
|
||||
|
||||
String nameIdFormat = samlClient.getNameIDFormat();
|
||||
if (nameIdFormat == null) nameIdFormat = SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT;
|
||||
Element spCertificate = SPMetadataDescriptor.buildKeyInfoElement(null, samlClient.getClientSigningCertificate());
|
||||
Element encCertificate = SPMetadataDescriptor.buildKeyInfoElement(null, samlClient.getClientEncryptingCertificate());
|
||||
return SPMetadataDescriptor.getSPDescriptor(binding, new URI(assertionUrl), new URI(logoutUrl), samlClient.requiresClientSignature(),
|
||||
return SPMetadataDescriptor.getSPDescriptor(loginBinding, logoutBinding, new URI(assertionUrl), new URI(logoutUrl), samlClient.requiresClientSignature(),
|
||||
samlClient.requiresAssertionSignature(), samlClient.requiresEncryption(),
|
||||
client.getClientId(), nameIdFormat, Arrays.asList(spCertificate), Arrays.asList(encCertificate));
|
||||
} catch (Exception ex) {
|
||||
|
|
|
@ -29,7 +29,7 @@ import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder;
|
|||
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
||||
import org.keycloak.protocol.saml.SamlProtocol;
|
||||
import org.keycloak.protocol.saml.SamlService;
|
||||
import org.keycloak.protocol.saml.profile.ecp.util.Soap;
|
||||
import org.keycloak.protocol.saml.profile.util.Soap;
|
||||
import org.keycloak.saml.SAML2LogoutResponseBuilder;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLConstants;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
|
@ -61,6 +61,10 @@ public class SamlEcpProfileService extends SamlService {
|
|||
}
|
||||
|
||||
public Response authenticate(InputStream inputStream) {
|
||||
return authenticate(Soap.extractSoapMessage(inputStream));
|
||||
}
|
||||
|
||||
public Response authenticate(Document soapMessage) {
|
||||
try {
|
||||
return new PostBindingProtocol() {
|
||||
@Override
|
||||
|
@ -80,7 +84,7 @@ public class SamlEcpProfileService extends SamlService {
|
|||
requestAbstractType.setDestination(session.getContext().getUri().getAbsolutePath());
|
||||
return super.loginRequest(relayState, requestAbstractType, client);
|
||||
}
|
||||
}.execute(Soap.toSamlHttpPostMessage(inputStream), null, null);
|
||||
}.execute(Soap.toSamlHttpPostMessage(soapMessage), null, null, null);
|
||||
} catch (Exception e) {
|
||||
String reason = "Some error occurred while processing the AuthnRequest.";
|
||||
String detail = e.getMessage();
|
||||
|
|
|
@ -15,18 +15,24 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.protocol.saml.profile.ecp.util;
|
||||
package org.keycloak.protocol.saml.profile.util;
|
||||
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.ByteArrayEntity;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.keycloak.saml.processing.core.saml.v2.util.DocumentUtil;
|
||||
import org.keycloak.saml.processing.web.util.PostBindingUtil;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import javax.xml.soap.MessageFactory;
|
||||
import javax.xml.soap.Name;
|
||||
import javax.xml.soap.SOAPBody;
|
||||
import javax.xml.soap.SOAPConnection;
|
||||
import javax.xml.soap.SOAPConnectionFactory;
|
||||
import javax.xml.soap.SOAPEnvelope;
|
||||
import javax.xml.soap.SOAPException;
|
||||
import javax.xml.soap.SOAPFault;
|
||||
|
@ -34,6 +40,7 @@ import javax.xml.soap.SOAPHeaderElement;
|
|||
import javax.xml.soap.SOAPMessage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
|
@ -54,21 +61,50 @@ public final class Soap {
|
|||
*
|
||||
* <p>The resulting string is based on the Body of the SOAP message, which should map to a valid SAML message.
|
||||
*
|
||||
* @param inputStream the input stream containing a valid SOAP message with a Body that contains a SAML message
|
||||
* @param document the document containing a valid SOAP message with a Body that contains a SAML message
|
||||
*
|
||||
* @return a string encoded accordingly with the SAML HTTP POST Binding specification
|
||||
*/
|
||||
public static String toSamlHttpPostMessage(InputStream inputStream) {
|
||||
public static String toSamlHttpPostMessage(Document document) {
|
||||
try {
|
||||
return PostBindingUtil.base64Encode(DocumentUtil.asString(document));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error encoding SOAP document to String.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns Docuemnt based on the given <code>inputStream</code> which must contain a valid SOAP message.
|
||||
*
|
||||
* <p>The resulting string is based on the Body of the SOAP message, which should map to a valid SAML message.
|
||||
*
|
||||
* @param inputStream an InputStream consisting of a SOAPMessage
|
||||
* @return A document containing the body of the SOAP message
|
||||
*/
|
||||
public static Document extractSoapMessage(InputStream inputStream) {
|
||||
try {
|
||||
MessageFactory messageFactory = MessageFactory.newInstance();
|
||||
SOAPMessage soapMessage = messageFactory.createMessage(null, inputStream);
|
||||
return extractSoapMessage(soapMessage);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error creating fault message.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns Docuemnt based on the given SOAP message.
|
||||
*
|
||||
* <p>The resulting string is based on the Body of the SOAP message, which should map to a valid SAML message.
|
||||
* @param soapMessage a SOAPMessage from which to extract the body
|
||||
* @return A document containing the body of the SOAP message
|
||||
*/
|
||||
public static Document extractSoapMessage(SOAPMessage soapMessage) {
|
||||
try {
|
||||
SOAPBody soapBody = soapMessage.getSOAPBody();
|
||||
Node authnRequestNode = soapBody.getFirstChild();
|
||||
Document document = DocumentUtil.createDocument();
|
||||
|
||||
document.appendChild(document.importNode(authnRequestNode, true));
|
||||
|
||||
return PostBindingUtil.base64Encode(DocumentUtil.asString(document));
|
||||
return document;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error creating fault message.", e);
|
||||
}
|
||||
|
@ -123,11 +159,7 @@ public final class Soap {
|
|||
}
|
||||
}
|
||||
|
||||
public Response build() {
|
||||
return build(Status.OK);
|
||||
}
|
||||
|
||||
Response build(Status status) {
|
||||
public byte[] getBytes() {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
|
@ -135,11 +167,55 @@ public final class Soap {
|
|||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error while building SOAP Fault.", e);
|
||||
}
|
||||
|
||||
return Response.status(status).entity(outputStream.toByteArray()).build();
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
SOAPMessage getMessage() {
|
||||
public Response build() {
|
||||
return build(Status.OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard build method, generates a javax ws rs Response
|
||||
* @param status the status of the response
|
||||
* @return a Response containing the SOAP message
|
||||
*/
|
||||
Response build(Status status) {
|
||||
return Response.status(status).entity(getBytes()).type(MediaType.TEXT_XML_TYPE).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build method for testing, generates an appache httpcomponents HttpPost
|
||||
* @param uri the URI to which to POST the soap message
|
||||
* @return an HttpPost containing the SOAP message
|
||||
*/
|
||||
public HttpPost buildHttpPost(URI uri) {
|
||||
HttpPost post = new HttpPost(uri);
|
||||
post.setEntity(new ByteArrayEntity(getBytes(), ContentType.TEXT_XML));
|
||||
return post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a synchronous call, sending the current message to the given url
|
||||
* @param url a SOAP endpoint url
|
||||
* @return the SOAPMessage returned by the contacted SOAP server
|
||||
* @throws SOAPException Raised if there's a problem performing the SOAP call
|
||||
*/
|
||||
public SOAPMessage call(String url) throws SOAPException {
|
||||
SOAPMessage response;
|
||||
SOAPConnection soapConnection = null;
|
||||
try {
|
||||
SOAPConnectionFactory soapConnectionFactory = SOAPConnectionFactory.newInstance();
|
||||
soapConnection = soapConnectionFactory.createConnection();
|
||||
response = soapConnection.call(message, url);
|
||||
} finally {
|
||||
if (soapConnection != null) {
|
||||
soapConnection.close();
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public SOAPMessage getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
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]);
|
||||
}
|
||||
}
|
|
@ -68,6 +68,7 @@ import org.keycloak.protocol.oidc.BackchannelLogoutResponse;
|
|||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.protocol.saml.SamlClient;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.Urls;
|
||||
|
@ -387,6 +388,7 @@ public class AuthenticationManager {
|
|||
Set<AuthenticatedClientSessionModel> notLoggedOutSessions = acs.entrySet().stream()
|
||||
.filter(me -> ! Objects.equals(AuthenticationSessionModel.Action.LOGGED_OUT, getClientLogoutAction(logoutAuthSession, me.getKey())))
|
||||
.filter(me -> ! Objects.equals(AuthenticationSessionModel.Action.LOGGED_OUT.name(), me.getValue().getAction()))
|
||||
.filter(me -> ! Objects.equals(AuthenticationSessionModel.Action.LOGGING_OUT.name(), me.getValue().getAction()))
|
||||
.filter(me -> Objects.nonNull(me.getValue().getProtocol())) // Keycloak service-like accounts
|
||||
.map(Map.Entry::getValue)
|
||||
.collect(Collectors.toSet());
|
||||
|
@ -473,7 +475,7 @@ public class AuthenticationManager {
|
|||
UserSessionModel userSession = clientSession.getUserSession();
|
||||
ClientModel client = clientSession.getClient();
|
||||
|
||||
if (! client.isFrontchannelLogout() || AuthenticationSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) {
|
||||
if (!client.isFrontchannelLogout() || AuthenticationSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -500,7 +502,9 @@ public class AuthenticationManager {
|
|||
logger.debug("returning frontchannel logout request to client");
|
||||
// setting this to logged out cuz I'm not sure protocols can always verify that the client was logged out or not
|
||||
|
||||
setClientLogoutAction(logoutAuthSession, client.getId(), AuthenticationSessionModel.Action.LOGGED_OUT);
|
||||
if (!AuthenticationSessionModel.Action.LOGGING_OUT.name().equals(clientSession.getAction())) {
|
||||
setClientLogoutAction(logoutAuthSession, client.getId(), AuthenticationSessionModel.Action.LOGGED_OUT);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
@ -599,7 +603,8 @@ public class AuthenticationManager {
|
|||
|
||||
private static Response browserLogoutAllClients(UserSessionModel userSession, KeycloakSession session, RealmModel realm, HttpHeaders headers, UriInfo uriInfo, AuthenticationSessionModel logoutAuthSession) {
|
||||
Map<Boolean, List<AuthenticatedClientSessionModel>> acss = userSession.getAuthenticatedClientSessions().values().stream()
|
||||
.filter(clientSession -> ! Objects.equals(AuthenticationSessionModel.Action.LOGGED_OUT.name(), clientSession.getAction()))
|
||||
.filter(clientSession -> !Objects.equals(AuthenticationSessionModel.Action.LOGGED_OUT.name(), clientSession.getAction())
|
||||
&& !Objects.equals(AuthenticationSessionModel.Action.LOGGING_OUT.name(), clientSession.getAction()))
|
||||
.filter(clientSession -> clientSession.getProtocol() != null)
|
||||
.collect(Collectors.partitioningBy(clientSession -> clientSession.getClient().isFrontchannelLogout()));
|
||||
|
||||
|
@ -623,9 +628,10 @@ public class AuthenticationManager {
|
|||
|
||||
checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession);
|
||||
|
||||
// For resolving artifact we don't need any cookie, all details are stored in session storage so we can remove
|
||||
expireIdentityCookie(realm, uriInfo, connection);
|
||||
expireRememberMeCookie(realm, uriInfo, connection);
|
||||
userSession.setState(UserSessionModel.State.LOGGED_OUT);
|
||||
|
||||
String method = userSession.getNote(KEYCLOAK_LOGOUT_PROTOCOL);
|
||||
EventBuilder event = new EventBuilder(realm, session, connection);
|
||||
LoginProtocol protocol = session.getProvider(LoginProtocol.class, method);
|
||||
|
@ -633,11 +639,50 @@ public class AuthenticationManager {
|
|||
.setHttpHeaders(headers)
|
||||
.setUriInfo(uriInfo)
|
||||
.setEventBuilder(event);
|
||||
|
||||
|
||||
Response response = protocol.finishLogout(userSession);
|
||||
session.sessions().removeUserSession(realm, userSession);
|
||||
|
||||
// It may be possible that there are some client sessions that are still in LOGGING_OUT state
|
||||
long numberOfUnconfirmedSessions = userSession.getAuthenticatedClientSessions().values().stream()
|
||||
.filter(clientSessionModel -> CommonClientSessionModel.Action.LOGGING_OUT.name().equals(clientSessionModel.getAction()))
|
||||
.count();
|
||||
|
||||
// If logout flow end up correctly there should be at maximum 1 client session in LOGGING_OUT action, if there are more, something went wrong
|
||||
if (numberOfUnconfirmedSessions > 1) {
|
||||
logger.warnf("There are more than one clientSession in logging_out state. Perhaps some client did not finish logout flow correctly.");
|
||||
}
|
||||
|
||||
// By setting LOGGED_OUT_UNCONFIRMED state we are saying that anybody who will turn the last client session into
|
||||
// LOGGED_OUT action can remove UserSession
|
||||
if (numberOfUnconfirmedSessions >= 1) {
|
||||
userSession.setState(UserSessionModel.State.LOGGED_OUT_UNCONFIRMED);
|
||||
} else {
|
||||
userSession.setState(UserSessionModel.State.LOGGED_OUT);
|
||||
}
|
||||
|
||||
// Do not remove user session, it will be removed when last clientSession will be logged out
|
||||
if (numberOfUnconfirmedSessions < 1) {
|
||||
session.sessions().removeUserSession(realm, userSession);
|
||||
}
|
||||
|
||||
session.authenticationSessions().removeRootAuthenticationSession(realm, logoutAuthSession.getParentSession());
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public static void finishUnconfirmedUserSession(KeycloakSession session, RealmModel realm, UserSessionModel userSessionModel) {
|
||||
if (userSessionModel.getAuthenticatedClientSessions().values().stream().anyMatch(cs -> !CommonClientSessionModel.Action.LOGGED_OUT.name().equals(cs.getAction()))) {
|
||||
logger.warnf("UserSession with id %s is removed while there are still some user sessions that are not logged out properly.", userSessionModel.getId());
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Client sessions with their states:");
|
||||
userSessionModel.getAuthenticatedClientSessions().values()
|
||||
.forEach(clientSessionModel -> logger.tracef("Client session for clientId: %s has action: %s", clientSessionModel.getClient().getClientId(), clientSessionModel.getAction()));
|
||||
}
|
||||
}
|
||||
|
||||
session.sessions().removeUserSession(realm, userSessionModel);
|
||||
}
|
||||
|
||||
|
||||
public static IdentityCookieToken createIdentityToken(KeycloakSession keycloakSession, RealmModel realm, UserModel user, UserSessionModel session, String issuer) {
|
||||
|
|
|
@ -246,6 +246,9 @@ public class Messages {
|
|||
public static final String DELEGATION_FAILED = "delegationFailedMessage";
|
||||
public static final String DELEGATION_FAILED_HEADER = "delegationFailedHeader";
|
||||
|
||||
public static final String ARTIFACT_RESOLUTION_SERVICE_ERROR = "artifactResolutionServiceError";
|
||||
public static final String ARTIFACT_RESOLUTION_SERVICE_INVALID_RESPONSE = "saml.artifactResolutionServiceInvalidResponse";
|
||||
|
||||
// WebAuthn
|
||||
public static final String WEBAUTHN_REGISTER_TITLE = "webauthn-registration-title";
|
||||
public static final String WEBAUTHN_LOGIN_TITLE = "webauthn-login-title";
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
package org.keycloak.transaction;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakTransaction;
|
||||
import org.keycloak.models.KeycloakTransactionManager;
|
||||
import org.keycloak.services.ErrorPage;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
|
||||
import javax.ws.rs.container.AsyncResponse;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
/**
|
||||
* When using {@link AsyncResponse#resume(Object)} directly in the code, the response is returned before all changes
|
||||
* done withing this execution are committed. Therefore we need some mechanism that resumes the AsyncResponse after all
|
||||
* changes are successfully committed. This can be achieved by enlisting an instance of AsyncResponseTransaction into
|
||||
* the main transaction using {@link org.keycloak.models.KeycloakTransactionManager#enlistAfterCompletion(KeycloakTransaction)}.
|
||||
*/
|
||||
public class AsyncResponseTransaction implements KeycloakTransaction {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final AsyncResponse responseToFinishInTransaction;
|
||||
private final Response responseToSend;
|
||||
|
||||
/**
|
||||
* This method creates a new AsyncResponseTransaction instance that resumes provided AsyncResponse
|
||||
* {@code responseToFinishInTransaction} with given Response {@code responseToSend}. The transaction is enlisted
|
||||
* to {@link KeycloakTransactionManager}.
|
||||
*
|
||||
* @param session Current KeycloakSession
|
||||
* @param responseToFinishInTransaction AsyncResponse to be resumed on {@link KeycloakTransactionManager} commit/rollback.
|
||||
* @param responseToSend Response to be sent
|
||||
*/
|
||||
public static void finishAsyncResponseInTransaction(KeycloakSession session, AsyncResponse responseToFinishInTransaction, Response responseToSend) {
|
||||
session.getTransactionManager().enlistAfterCompletion(new AsyncResponseTransaction(session, responseToFinishInTransaction, responseToSend));
|
||||
}
|
||||
|
||||
private AsyncResponseTransaction(KeycloakSession session, AsyncResponse responseToFinishInTransaction, Response responseToSend) {
|
||||
this.session = session;
|
||||
this.responseToFinishInTransaction = responseToFinishInTransaction;
|
||||
this.responseToSend = responseToSend;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void begin() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit() {
|
||||
responseToFinishInTransaction.resume(responseToSend);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollback() {
|
||||
responseToFinishInTransaction.resume(ErrorPage.error(session, null, Response.Status.INTERNAL_SERVER_ERROR, Messages.INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRollbackOnly() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getRollbackOnly() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
#
|
||||
# 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.
|
||||
#
|
||||
#
|
||||
|
||||
org.keycloak.protocol.saml.DefaultSamlArtifactResolverFactory
|
|
@ -0,0 +1,63 @@
|
|||
package org.keycloak.protocol.saml;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.saml.SAML2LoginResponseBuilder;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
import org.keycloak.saml.common.util.DocumentUtil;
|
||||
import org.keycloak.saml.common.util.StaxUtil;
|
||||
import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
|
||||
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLResponseWriter;
|
||||
import org.w3c.dom.Document;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.lessThan;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
public class SamlProtocolUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testBuildArtifactResponse() throws ConfigurationException, ProcessingException, ParsingException {
|
||||
|
||||
ResponseType response = new SAML2LoginResponseBuilder()
|
||||
.requestID(IDGenerator.create("ID_"))
|
||||
.destination("http://localhost:8180/auth/realms/demo/broker/saml-broker/endpoint")
|
||||
.issuer("http://saml.idp/saml")
|
||||
.assertionExpiration(1000000)
|
||||
.subjectExpiration(1000000)
|
||||
.requestIssuer("http://localhost:8180/auth/realms/demo")
|
||||
.nameIdentifier(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get(), "a@b.c")
|
||||
.authMethod(JBossSAMLURIConstants.AC_UNSPECIFIED.get())
|
||||
.sessionIndex("idp:" + UUID.randomUUID())
|
||||
.buildModel();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
SAMLResponseWriter writer = new SAMLResponseWriter(StaxUtil.getXMLStreamWriter(bos));
|
||||
writer.write(response);
|
||||
Document responseDoc = DocumentUtil.getDocument(new ByteArrayInputStream(bos.toByteArray()));
|
||||
|
||||
ArtifactResponseType artifactResponseType = SamlProtocolUtils.buildArtifactResponse(responseDoc);
|
||||
Document doc = SamlProtocolUtils.convert(artifactResponseType);
|
||||
String artifactResponse = DocumentUtil.asString(doc);
|
||||
|
||||
assertThat(artifactResponse, containsString("samlp:ArtifactResponse"));
|
||||
assertThat(artifactResponse, containsString("samlp:Response"));
|
||||
assertThat(artifactResponse, containsString("saml:Assertion"));
|
||||
assertThat(artifactResponse.indexOf("samlp:ArtifactResponse"), lessThan(artifactResponse.indexOf("samlp:Response")));
|
||||
assertThat(artifactResponse.indexOf("samlp:Response"), lessThan(artifactResponse.indexOf("saml:Assertion")));
|
||||
assertThat(artifactResponse.split("\\Q<saml:Issuer>http://saml.idp/saml</saml:Issuer>\\E").length, is(4));
|
||||
assertThat(artifactResponse.split(
|
||||
"\\Q<samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/>\\E").length, is(3));
|
||||
}
|
||||
|
||||
}
|
|
@ -22,3 +22,7 @@ echo ** Adding spi=userProfile with legacy-user-profile configuration of read-on
|
|||
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:add(properties={},enabled=true)
|
||||
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:map-put(name=properties,key=read-only-attributes,value=[deniedFoo,deniedBar*,deniedSome/thing,deniedsome*thing])
|
||||
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:map-put(name=properties,key=admin-read-only-attributes,value=[deniedSomeAdmin])
|
||||
|
||||
echo ** Do not reuse connections for HttpClientProvider within testsuite **
|
||||
/subsystem=keycloak-server/spi=connectionsHttpClient/provider=default/:map-put(name=properties,key=reuse-connections,value=false)
|
||||
|
||||
|
|
|
@ -21,4 +21,7 @@ spi.hostname.default.frontend-url = ${keycloak.frontendUrl:}
|
|||
|
||||
# Truststore Provider
|
||||
spi.truststore.file.file=${kc.home.dir}/conf/keycloak.truststore
|
||||
spi.truststore.file.password=secret
|
||||
spi.truststore.file.password=secret
|
||||
|
||||
# http client connection reuse settings
|
||||
spi.connections-http-client.default.reuse-connections=false
|
||||
|
|
|
@ -57,7 +57,6 @@
|
|||
<groupId>org.wildfly.core</groupId>
|
||||
<artifactId>wildfly-controller</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
package org.keycloak.testsuite.authentication;
|
||||
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.protocol.saml.ArtifactResolver;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* This ArtifactResolver should be used only for testing purposes.
|
||||
*/
|
||||
public class CustomTestingSamlArtifactResolver implements ArtifactResolver {
|
||||
|
||||
public static List<String> list = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public ClientModel selectSourceClient(String artifact, Stream<ClientModel> clients) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildArtifact(AuthenticatedClientSessionModel clientSessionModel, String entityId, String artifactResponse) {
|
||||
int artifactIndex = list.size();
|
||||
list.add(artifactResponse);
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
try {
|
||||
bos.write(TYPE_CODE);
|
||||
bos.write(artifactIndex);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
byte[] artifact = bos.toByteArray();
|
||||
return Base64.getEncoder().encodeToString(artifact);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolveArtifact(AuthenticatedClientSessionModel clientSessionModel, String artifact) {
|
||||
byte[] byteArray = Base64.getDecoder().decode(artifact);
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(byteArray);
|
||||
bis.skip(2);
|
||||
int index = bis.read();
|
||||
|
||||
return list.get(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package org.keycloak.testsuite.authentication;
|
||||
|
||||
import org.keycloak.Config;
|
||||
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;
|
||||
|
||||
/**
|
||||
* This ArtifactResolver should be used only for testing purposes.
|
||||
*/
|
||||
public class CustomTestingSamlArtifactResolverFactory implements ArtifactResolverFactory {
|
||||
|
||||
public static final byte[] TYPE_CODE = {0, 5};
|
||||
public static final CustomTestingSamlArtifactResolver resolver = new CustomTestingSamlArtifactResolver();
|
||||
|
||||
@Override
|
||||
public ArtifactResolver create(KeycloakSession session) {
|
||||
return resolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ArtifactBindingUtils.byteArrayToResolverProviderId(TYPE_CODE);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
#
|
||||
# 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.
|
||||
#
|
||||
#
|
||||
|
||||
org.keycloak.testsuite.authentication.CustomTestingSamlArtifactResolverFactory
|
|
@ -45,11 +45,13 @@ import org.keycloak.admin.client.Keycloak;
|
|||
import org.keycloak.common.util.StringPropertyReplacer;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.services.error.KeycloakErrorHandler;
|
||||
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
|
||||
import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableVault;
|
||||
import org.keycloak.testsuite.client.KeycloakTestingClient;
|
||||
import org.keycloak.testsuite.util.LogChecker;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.SpiProvidersSwitchingUtils;
|
||||
import org.keycloak.testsuite.util.SqlUtils;
|
||||
import org.keycloak.testsuite.util.SystemInfoHelper;
|
||||
import org.keycloak.testsuite.util.VaultUtils;
|
||||
|
@ -350,6 +352,19 @@ public class AuthServerTestEnricher {
|
|||
}
|
||||
}
|
||||
|
||||
public static void executeCli(String... commands) throws Exception {
|
||||
OnlineManagementClient client = AuthServerTestEnricher.getManagementClient();
|
||||
Administration administration = new Administration(client);
|
||||
|
||||
for (String c : commands) {
|
||||
client.execute(c).assertSuccess();
|
||||
}
|
||||
|
||||
administration.reload();
|
||||
|
||||
client.close();
|
||||
}
|
||||
|
||||
private ContainerInfo updateWithAuthServerInfo(ContainerInfo authServerInfo) {
|
||||
return updateWithAuthServerInfo(authServerInfo, 0);
|
||||
}
|
||||
|
@ -531,10 +546,22 @@ public class AuthServerTestEnricher {
|
|||
TestContext testContext = new TestContext(suiteContext, event.getTestClass().getJavaClass());
|
||||
testContextProducer.set(testContext);
|
||||
|
||||
if (!isAuthServerRemote() && !isAuthServerQuarkus() && event.getTestClass().isAnnotationPresent(EnableVault.class)) {
|
||||
VaultUtils.enableVault(suiteContext, event.getTestClass().getAnnotation(EnableVault.class).providerId());
|
||||
restartAuthServer();
|
||||
testContext.reconnectAdminClient();
|
||||
if (!isAuthServerRemote() && !isAuthServerQuarkus()) {
|
||||
boolean wasUpdated = false;
|
||||
|
||||
if (event.getTestClass().isAnnotationPresent(EnableVault.class)) {
|
||||
VaultUtils.enableVault(suiteContext, event.getTestClass().getAnnotation(EnableVault.class).providerId());
|
||||
wasUpdated = true;
|
||||
}
|
||||
if (event.getTestClass().isAnnotationPresent(SetDefaultProvider.class)) {
|
||||
SpiProvidersSwitchingUtils.addProviderDefaultValue(suiteContext, event.getTestClass().getAnnotation(SetDefaultProvider.class));
|
||||
wasUpdated = true;
|
||||
}
|
||||
|
||||
if (wasUpdated) {
|
||||
restartAuthServer();
|
||||
testContext.reconnectAdminClient();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -851,10 +878,23 @@ public class AuthServerTestEnricher {
|
|||
|
||||
removeTestRealms(testContext, adminClient);
|
||||
|
||||
if (!isAuthServerRemote() && event.getTestClass().isAnnotationPresent(EnableVault.class)) {
|
||||
VaultUtils.disableVault(suiteContext, event.getTestClass().getAnnotation(EnableVault.class).providerId());
|
||||
restartAuthServer();
|
||||
testContext.reconnectAdminClient();
|
||||
if (!isAuthServerRemote() && !isAuthServerQuarkus()) {
|
||||
|
||||
boolean wasUpdated = false;
|
||||
if (event.getTestClass().isAnnotationPresent(EnableVault.class)) {
|
||||
VaultUtils.disableVault(suiteContext, event.getTestClass().getAnnotation(EnableVault.class).providerId());
|
||||
wasUpdated = true;
|
||||
}
|
||||
|
||||
if (event.getTestClass().isAnnotationPresent(SetDefaultProvider.class)) {
|
||||
SpiProvidersSwitchingUtils.removeProvider(suiteContext, event.getTestClass().getAnnotation(SetDefaultProvider.class));
|
||||
wasUpdated = true;
|
||||
}
|
||||
|
||||
if (wasUpdated) {
|
||||
restartAuthServer();
|
||||
testContext.reconnectAdminClient();
|
||||
}
|
||||
}
|
||||
|
||||
if (adminClient != null) {
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package org.keycloak.testsuite.arquillian.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.TYPE})
|
||||
public @interface SetDefaultProvider {
|
||||
String spi();
|
||||
String providerId();
|
||||
}
|
|
@ -42,6 +42,11 @@ public class RealmAttributeUpdater extends ServerResourceUpdater<RealmAttributeU
|
|||
rep.setDefaultDefaultClientScopes(defaultClientScopes);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmAttributeUpdater setAccessCodeLifespan(Integer accessCodeLifespan) {
|
||||
rep.setAccessCodeLifespan(accessCodeLifespan);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmAttributeUpdater setSsoSessionIdleTimeout(Integer timeout) {
|
||||
rep.setSsoSessionIdleTimeout(timeout);
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
package org.keycloak.testsuite.util;
|
||||
|
||||
import org.keycloak.dom.saml.v2.protocol.ArtifactResolveType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
|
||||
import org.keycloak.protocol.saml.SamlProtocolUtils;
|
||||
import org.keycloak.protocol.saml.profile.util.Soap;
|
||||
import org.keycloak.saml.SAML2NameIDBuilder;
|
||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
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 org.w3c.dom.Document;
|
||||
|
||||
import javax.xml.transform.Source;
|
||||
import javax.xml.transform.Transformer;
|
||||
import javax.xml.transform.TransformerException;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
import javax.xml.transform.stream.StreamSource;
|
||||
import javax.xml.ws.Endpoint;
|
||||
import javax.xml.ws.Provider;
|
||||
import javax.xml.ws.Service;
|
||||
import javax.xml.ws.ServiceMode;
|
||||
import javax.xml.ws.WebServiceProvider;
|
||||
import javax.xml.ws.http.HTTPBinding;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* This class simulates a service provider's (clients's) Artifact Resolution Service. It is a webservice provider
|
||||
* that can accept an artifact resolve message, and return an artifact response (all via SOAP).
|
||||
*
|
||||
* The class in runnable, and must be run in a thread before being used. The calling test SHOULD perform a wait
|
||||
* on its instance of the class before proceeding, as this class will notify when it as finished setting up the endpoint
|
||||
*/
|
||||
@WebServiceProvider
|
||||
@ServiceMode(value = Service.Mode.MESSAGE)
|
||||
public class ArtifactResolutionService implements Provider<Source>, Runnable {
|
||||
|
||||
private ArtifactResponseType artifactResponseType;
|
||||
private final String endpointAddress;
|
||||
private ArtifactResolveType lastArtifactResolve;
|
||||
private boolean running = true;
|
||||
|
||||
/**
|
||||
* Standard constructor
|
||||
* @param endpointAddress full address on which this endpoint will listen for SOAP messages (e.g. http://127.0.0.1:8082/)
|
||||
*/
|
||||
public ArtifactResolutionService(String endpointAddress) {
|
||||
this.endpointAddress = endpointAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the SAML message that will be integrated into the artifact response
|
||||
* @param responseDocument a Document of the SAML message
|
||||
* @return this ArtifactResolutionService
|
||||
*/
|
||||
public ArtifactResolutionService setResponseDocument(Document responseDocument){
|
||||
try {
|
||||
this.artifactResponseType = SamlProtocolUtils.buildArtifactResponse(responseDocument);
|
||||
} catch (ParsingException | ProcessingException | ConfigurationException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ArtifactResolutionService setEmptyArtifactResponse(String issuer) {
|
||||
try {
|
||||
this.artifactResponseType = SamlProtocolUtils.buildArtifactResponse(null, SAML2NameIDBuilder.value(issuer).build());
|
||||
} catch (ConfigurationException | ProcessingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ArtifactResolve message that was last received by the service. If received data was not an
|
||||
* ArtifactResolve message, the value returned will be null
|
||||
* @return the last received ArtifactResolve message
|
||||
*/
|
||||
public ArtifactResolveType getLastArtifactResolve() {
|
||||
return lastArtifactResolve;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the method called when a message is received by the endpoint.
|
||||
* It gets the message, extracts the ArtifactResolve message from the SOAP, creates a SOAP message containing
|
||||
* an ArtifactResponse message with the configured SAML message, and returns it.
|
||||
* @param msg The SOAP message received by the endpoint, in Source format
|
||||
* @return A StreamSource containing the ArtifactResponse
|
||||
*/
|
||||
@Override
|
||||
public Source invoke(Source msg) {
|
||||
byte[] response;
|
||||
|
||||
try (StringWriter w = new StringWriter()){
|
||||
Transformer trans = TransformerFactory.newInstance().newTransformer();
|
||||
trans.transform(msg, new StreamResult(w));
|
||||
String s = w.toString();
|
||||
Document doc = Soap.extractSoapMessage(new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)));
|
||||
SAMLDocumentHolder samlDoc = SAML2Request.getSAML2ObjectFromDocument(doc);
|
||||
if (samlDoc.getSamlObject() instanceof ArtifactResolveType) {
|
||||
lastArtifactResolve = (ArtifactResolveType) samlDoc.getSamlObject();
|
||||
} else {
|
||||
lastArtifactResolve = null;
|
||||
}
|
||||
Document artifactResponse = SamlProtocolUtils.convert(artifactResponseType);
|
||||
response = Soap.createMessage().addToBody(artifactResponse).getBytes();
|
||||
} catch (ProcessingException | ConfigurationException | TransformerException | ParsingException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return new StreamSource(new ByteArrayInputStream(response));
|
||||
}
|
||||
|
||||
/**
|
||||
* Main method of the class. Creates the endpoint, and will keep it running until the "stop()" method is called.
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
Endpoint endpoint;
|
||||
synchronized (this) {
|
||||
endpoint = Endpoint.create(HTTPBinding.HTTP_BINDING, this);
|
||||
endpoint.publish(endpointAddress);
|
||||
notify();
|
||||
}
|
||||
while (running) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
endpoint.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calling this method will allow the run method to shutdown gracefully
|
||||
*/
|
||||
public void stop() {
|
||||
running = false;
|
||||
}
|
||||
}
|
|
@ -31,13 +31,21 @@ import org.apache.http.impl.client.HttpClientBuilder;
|
|||
import org.apache.http.impl.client.LaxRedirectStrategy;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
import org.keycloak.adapters.saml.SamlDeployment;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.util.KeyUtils;
|
||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
|
||||
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
||||
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.protocol.saml.SamlProtocolUtils;
|
||||
import org.keycloak.protocol.saml.profile.util.Soap;
|
||||
import org.keycloak.rotation.KeyLocator;
|
||||
import org.keycloak.saml.BaseSAML2BindingBuilder;
|
||||
import org.keycloak.saml.SAMLRequestParser;
|
||||
import org.keycloak.saml.SignatureAlgorithm;
|
||||
|
@ -47,14 +55,27 @@ import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
|||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
import org.keycloak.saml.common.util.DocumentUtil;
|
||||
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
|
||||
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.keycloak.saml.processing.core.saml.v2.util.DocumentUtil;
|
||||
import org.keycloak.saml.processing.core.util.JAXPValidationUtil;
|
||||
import org.keycloak.saml.processing.web.util.RedirectBindingUtil;
|
||||
import org.keycloak.testsuite.util.saml.StepWithCheckers;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedHashMap;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.xml.soap.MessageFactory;
|
||||
import javax.xml.soap.SOAPBody;
|
||||
import javax.xml.soap.SOAPEnvelope;
|
||||
import javax.xml.soap.SOAPException;
|
||||
import javax.xml.soap.SOAPHeader;
|
||||
import javax.xml.soap.SOAPHeaderElement;
|
||||
import javax.xml.soap.SOAPMessage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
@ -68,29 +89,15 @@ import java.util.Arrays;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import javax.ws.rs.core.MultivaluedHashMap;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.xml.soap.MessageFactory;
|
||||
import javax.xml.soap.SOAPBody;
|
||||
import javax.xml.soap.SOAPEnvelope;
|
||||
import javax.xml.soap.SOAPException;
|
||||
import javax.xml.soap.SOAPHeader;
|
||||
import javax.xml.soap.SOAPHeaderElement;
|
||||
import javax.xml.soap.SOAPMessage;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.protocol.saml.SamlProtocolUtils;
|
||||
import org.keycloak.rotation.KeyLocator;
|
||||
|
||||
import static org.keycloak.saml.common.constants.GeneralConstants.RELAY_STATE;
|
||||
import org.keycloak.saml.processing.web.util.RedirectBindingUtil;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
|
||||
import static org.keycloak.testsuite.util.SamlUtils.getSamlDeploymentForClient;
|
||||
|
||||
/**
|
||||
* @author hmlnarik
|
||||
|
@ -446,7 +453,7 @@ public class SamlClient {
|
|||
envelope.addNamespaceDeclaration(NS_PREFIX_PAOS_BINDING, JBossSAMLURIConstants.PAOS_BINDING.get());
|
||||
envelope.addNamespaceDeclaration(NS_PREFIX_PROFILE_ECP, JBossSAMLURIConstants.ECP_PROFILE.get());
|
||||
|
||||
SamlDeployment deployment = SamlUtils.getSamlDeploymentForClient("ecp-sp"); // TODO: Make more general for any client, currently SOAP is usable only with http://localhost:8280/ecp-sp/ client
|
||||
SamlDeployment deployment = getSamlDeploymentForClient("ecp-sp"); // TODO: Make more general for any client, currently SOAP is usable only with http://localhost:8280/ecp-sp/ client
|
||||
|
||||
createPaosRequestHeader(envelope, deployment);
|
||||
createEcpRequestHeader(envelope, deployment);
|
||||
|
@ -490,8 +497,88 @@ public class SamlClient {
|
|||
public String extractRelayState(CloseableHttpResponse response) throws IOException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
;
|
||||
},
|
||||
ARTIFACT_RESPONSE {
|
||||
private Document extractSoapMessage(CloseableHttpResponse response) throws IOException {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(EntityUtils.toByteArray(response.getEntity()));
|
||||
Document soapBody = Soap.extractSoapMessage(bais);
|
||||
response.close();
|
||||
return soapBody;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SAMLDocumentHolder extractResponse(CloseableHttpResponse response, String realmPublicKey) throws IOException {
|
||||
assertThat(response, statusCodeIsHC(Response.Status.OK));
|
||||
Document soapBodyContents = extractSoapMessage(response);
|
||||
|
||||
SAMLDocumentHolder samlDoc = null;
|
||||
try {
|
||||
samlDoc = SAML2Request.getSAML2ObjectFromDocument(soapBodyContents);
|
||||
} catch (ProcessingException | ParsingException e) {
|
||||
throw new RuntimeException("Unable to get documentHolder from soapBodyResponse: " + DocumentUtil.asString(soapBodyContents));
|
||||
}
|
||||
if (!(samlDoc.getSamlObject() instanceof ArtifactResponseType)) {
|
||||
throw new RuntimeException("Message received from ArtifactResolveService is not an ArtifactResponseMessage");
|
||||
}
|
||||
|
||||
ArtifactResponseType art = (ArtifactResponseType) samlDoc.getSamlObject();
|
||||
|
||||
try {
|
||||
Object artifactResponseContent = art.getAny();
|
||||
if (artifactResponseContent instanceof ResponseType) {
|
||||
Document doc = SAML2Request.convert((ResponseType) artifactResponseContent);
|
||||
return new SAMLDocumentHolder((ResponseType) artifactResponseContent, doc);
|
||||
} else if (artifactResponseContent instanceof RequestAbstractType) {
|
||||
Document doc = SAML2Request.convert((RequestAbstractType) art.getAny());
|
||||
return new SAMLDocumentHolder((RequestAbstractType) artifactResponseContent, doc);
|
||||
} else {
|
||||
throw new RuntimeException("Can not recognise message contained in ArtifactResponse");
|
||||
}
|
||||
} catch (ParsingException | ConfigurationException | ProcessingException e) {
|
||||
throw new RuntimeException("Can not obtain document from artifact response: " + DocumentUtil.asString(soapBodyContents));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpUriRequest createSamlUnsignedRequest(URI samlEndpoint, String relayState, Document samlRequest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpUriRequest createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpUriRequest createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey, String certificateStr) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getBindingUri() {
|
||||
return JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpUriRequest createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpUriRequest createSamlSignedResponse(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpUriRequest createSamlSignedResponse(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey, String certificateStr) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String extractRelayState(CloseableHttpResponse response) throws IOException {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
public abstract SAMLDocumentHolder extractResponse(CloseableHttpResponse response, String realmPublicKey) throws IOException;
|
||||
|
||||
|
@ -688,8 +775,19 @@ public class SamlClient {
|
|||
}
|
||||
|
||||
LOG.infof("Executing HTTP request to %s", request.getURI());
|
||||
|
||||
if (s instanceof StepWithCheckers) {
|
||||
Runnable beforeChecker = ((StepWithCheckers) s).getBeforeStepChecker();
|
||||
if (beforeChecker != null) beforeChecker.run();
|
||||
}
|
||||
|
||||
currentResponse = client.execute(request, context);
|
||||
|
||||
if (s instanceof StepWithCheckers) {
|
||||
Runnable afterChecker = ((StepWithCheckers) s).getAfterStepChecker();
|
||||
if (afterChecker != null) afterChecker.run();
|
||||
}
|
||||
|
||||
currentUri = request.getURI();
|
||||
List<URI> locations = context.getRedirectLocations();
|
||||
if (locations != null && ! locations.isEmpty()) {
|
||||
|
@ -716,6 +814,8 @@ public class SamlClient {
|
|||
}
|
||||
|
||||
protected HttpClientBuilder createHttpClientBuilderInstance() {
|
||||
return HttpClientBuilder.create();
|
||||
return HttpClientBuilder
|
||||
.create()
|
||||
.evictIdleConnections(100, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.keycloak.testsuite.util;
|
|||
|
||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.keycloak.testsuite.client.KeycloakTestingClient;
|
||||
import org.keycloak.testsuite.page.AbstractPage;
|
||||
import org.keycloak.testsuite.util.SamlClient.Binding;
|
||||
import org.keycloak.testsuite.util.SamlClient.DoNotFollowRedirectStep;
|
||||
|
@ -29,8 +30,10 @@ import java.util.List;
|
|||
import java.util.function.Consumer;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.keycloak.testsuite.util.saml.CreateArtifactMessageStepBuilder;
|
||||
import org.keycloak.testsuite.util.saml.CreateAuthnRequestStepBuilder;
|
||||
import org.keycloak.testsuite.util.saml.CreateLogoutRequestStepBuilder;
|
||||
import org.keycloak.testsuite.util.saml.HandleArtifactStepBuilder;
|
||||
import org.keycloak.testsuite.util.saml.IdPInitiatedLoginBuilder;
|
||||
import org.keycloak.testsuite.util.saml.LoginBuilder;
|
||||
import org.keycloak.testsuite.util.saml.UpdateProfileBuilder;
|
||||
|
@ -105,7 +108,6 @@ public class SamlClientBuilder {
|
|||
|
||||
/**
|
||||
* Adds a single generic step
|
||||
* @param step
|
||||
* @return This builder
|
||||
*/
|
||||
public SamlClientBuilder addStep(Runnable stepWithNoParameters) {
|
||||
|
@ -259,4 +261,20 @@ public class SamlClientBuilder {
|
|||
});
|
||||
}
|
||||
|
||||
public HandleArtifactStepBuilder handleArtifact(URI authServerSamlUrl, String issuer) {
|
||||
return doNotFollowRedirects()
|
||||
.addStepBuilder(new HandleArtifactStepBuilder(authServerSamlUrl, issuer, this));
|
||||
}
|
||||
|
||||
public HandleArtifactStepBuilder handleArtifact(HandleArtifactStepBuilder handleArtifactStepBuilder) {
|
||||
return doNotFollowRedirects().addStepBuilder(handleArtifactStepBuilder);
|
||||
}
|
||||
|
||||
public CreateArtifactMessageStepBuilder artifactMessage(URI authServerSamlUrl, String issuer, Binding requestBinding) {
|
||||
return addStepBuilder(new CreateArtifactMessageStepBuilder(authServerSamlUrl, issuer, requestBinding,this));
|
||||
}
|
||||
|
||||
public CreateArtifactMessageStepBuilder artifactMessage(CreateArtifactMessageStepBuilder camb) {
|
||||
return addStepBuilder(camb);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
package org.keycloak.testsuite.util;
|
||||
|
||||
import org.apache.tools.ant.filters.StringInputStream;
|
||||
import org.keycloak.adapters.saml.SamlDeployment;
|
||||
import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
|
||||
import org.keycloak.adapters.saml.config.parsers.ResourceLoader;
|
||||
import org.keycloak.admin.client.resource.ClientsResource;
|
||||
import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType;
|
||||
import org.keycloak.dom.saml.v2.metadata.SPSSODescriptorType;
|
||||
import org.keycloak.protocol.saml.installation.SamlSPDescriptorClientInstallation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
|
||||
import org.keycloak.testsuite.utils.arquillian.DeploymentArchiveProcessorUtils;
|
||||
import org.keycloak.testsuite.utils.io.IOUtil;
|
||||
import org.w3c.dom.Document;
|
||||
|
@ -32,4 +39,16 @@ public class SamlUtils {
|
|||
};
|
||||
return new DeploymentBuilder().build(isProcessed, loader);
|
||||
}
|
||||
|
||||
public static SPSSODescriptorType getSPInstallationDescriptor(ClientsResource res, String clientId) throws ParsingException {
|
||||
String spDescriptorString = res.findByClientId(clientId).stream().findFirst()
|
||||
.map(ClientRepresentation::getId)
|
||||
.map(res::get)
|
||||
.map(clientResource -> clientResource.getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR))
|
||||
.orElseThrow(() -> new RuntimeException("Missing descriptor"));
|
||||
|
||||
SAMLParser parser = SAMLParser.getInstance();
|
||||
EntityDescriptorType o = (EntityDescriptorType) parser.parse(new StringInputStream(spDescriptorString));
|
||||
return o.getChoiceType().get(0).getDescriptors().get(0).getSpDescriptor();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package org.keycloak.testsuite.util;
|
||||
|
||||
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
|
||||
import org.keycloak.testsuite.arquillian.SuiteContext;
|
||||
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
|
||||
import org.wildfly.extras.creaper.core.online.CliException;
|
||||
import org.wildfly.extras.creaper.core.online.OnlineManagementClient;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public class SpiProvidersSwitchingUtils {
|
||||
public static void addProviderDefaultValue(SuiteContext suiteContext, SetDefaultProvider annotation) throws IOException, CliException {
|
||||
if (suiteContext.getAuthServerInfo().isUndertow()) {
|
||||
System.setProperty("keycloak." + annotation.spi() + ".provider", annotation.providerId());
|
||||
} else {
|
||||
OnlineManagementClient client = AuthServerTestEnricher.getManagementClient();
|
||||
client.execute("/subsystem=keycloak-server/spi=" + annotation.spi() + "/:add(default-provider=\"" + annotation.providerId() + "\")");
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeProvider(SuiteContext suiteContext, SetDefaultProvider annotation) throws IOException, CliException {
|
||||
if (suiteContext.getAuthServerInfo().isUndertow()) {
|
||||
System.clearProperty("keycloak." + annotation.spi() + ".provider");
|
||||
} else {
|
||||
OnlineManagementClient client = AuthServerTestEnricher.getManagementClient();
|
||||
client.execute("/subsystem=keycloak-server/spi=" + annotation.spi() + "/:remove");
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package org.keycloak.testsuite.util.saml;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
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.jboss.logging.Logger;
|
||||
import org.keycloak.protocol.saml.ArtifactResolver;
|
||||
import org.keycloak.protocol.saml.ArtifactResolverProcessingException;
|
||||
import org.keycloak.protocol.saml.DefaultSamlArtifactResolver;
|
||||
import org.keycloak.protocol.saml.SamlProtocolUtils;
|
||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
import org.keycloak.testsuite.util.SamlClient;
|
||||
import org.keycloak.testsuite.util.SamlClientBuilder;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class CreateArtifactMessageStepBuilder implements SamlClient.Step {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(CreateArtifactMessageStepBuilder.class);
|
||||
|
||||
private final URI authServerSamlUrl;
|
||||
private final SamlClient.Binding requestBinding;
|
||||
private final SamlClientBuilder clientBuilder;
|
||||
private final String issuer;
|
||||
private String lastArtifact;
|
||||
private ArtifactResolver artifactResolver = new DefaultSamlArtifactResolver();
|
||||
|
||||
public CreateArtifactMessageStepBuilder(URI authServerSamlUrl, String issuer, SamlClient.Binding requestBinding, SamlClientBuilder clientBuilder) {
|
||||
this.authServerSamlUrl = authServerSamlUrl;
|
||||
this.requestBinding = requestBinding;
|
||||
this.clientBuilder = clientBuilder;
|
||||
this.issuer = issuer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
|
||||
DefaultSamlArtifactResolver artifactResolver = new DefaultSamlArtifactResolver();
|
||||
lastArtifact = artifactResolver.createArtifact(issuer);
|
||||
if (SamlClient.Binding.POST == requestBinding) {
|
||||
return sendArtifactMessagePost();
|
||||
}
|
||||
return sendArtifactMessageRedirect();
|
||||
}
|
||||
|
||||
private HttpUriRequest sendArtifactMessageRedirect() throws IOException, ProcessingException, URISyntaxException {
|
||||
URIBuilder builder = new URIBuilder(authServerSamlUrl)
|
||||
.setParameter(GeneralConstants.SAML_ARTIFACT_KEY, lastArtifact);
|
||||
|
||||
LOG.infof("Sending GET request with artifact %s", lastArtifact);
|
||||
return new HttpGet(builder.build());
|
||||
}
|
||||
|
||||
private HttpUriRequest sendArtifactMessagePost() throws IOException, ProcessingException {
|
||||
HttpPost post = new HttpPost(authServerSamlUrl);
|
||||
List<NameValuePair> parameters = new LinkedList<>();
|
||||
parameters.add(new BasicNameValuePair(GeneralConstants.SAML_ARTIFACT_KEY, lastArtifact));
|
||||
LOG.infof("Sending POST request with artifact %s", lastArtifact);
|
||||
|
||||
UrlEncodedFormEntity formEntity;
|
||||
try {
|
||||
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
post.setEntity(formEntity);
|
||||
|
||||
return post;
|
||||
}
|
||||
|
||||
public SamlClientBuilder build() {
|
||||
return clientBuilder;
|
||||
}
|
||||
|
||||
public String getLastArtifact() {
|
||||
return lastArtifact;
|
||||
}
|
||||
}
|
|
@ -49,6 +49,7 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
|
|||
private String signingPublicKeyPem; // TODO: should not be needed
|
||||
private String signingPrivateKeyPem;
|
||||
private String signingCertificate;
|
||||
private URI protocolBinding;
|
||||
private String authorizationHeader;
|
||||
|
||||
private final Document forceLoginRequestDocument;
|
||||
|
@ -86,6 +87,15 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
|
|||
return this;
|
||||
}
|
||||
|
||||
public CreateAuthnRequestStepBuilder setProtocolBinding(URI protocolBinding) {
|
||||
this.protocolBinding = protocolBinding;
|
||||
return this;
|
||||
}
|
||||
|
||||
public URI getProtocolBinding() {
|
||||
return protocolBinding;
|
||||
}
|
||||
|
||||
public CreateAuthnRequestStepBuilder signWith(String signingPrivateKeyPem, String signingPublicKeyPem) {
|
||||
return signWith(signingPrivateKeyPem, signingPublicKeyPem, null);
|
||||
}
|
||||
|
@ -96,7 +106,7 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
|
|||
this.signingCertificate = signingCertificate;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public CreateAuthnRequestStepBuilder basicAuthentication(UserRepresentation user) {
|
||||
String username = user.getUsername();
|
||||
String password = Users.getPasswordOf(user);
|
||||
|
@ -126,7 +136,7 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
|
|||
if (authorizationHeader != null) {
|
||||
request.addHeader(HttpHeaders.AUTHORIZATION, authorizationHeader);
|
||||
}
|
||||
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
|
@ -137,9 +147,10 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
|
|||
|
||||
try {
|
||||
SAML2Request samlReq = new SAML2Request();
|
||||
AuthnRequestType loginReq = samlReq.createAuthnRequestType(UUID.randomUUID().toString(),
|
||||
assertionConsumerURL, this.authServerSamlUrl.toString(), issuer, requestBinding.getBindingUri());
|
||||
|
||||
AuthnRequestType loginReq = samlReq.createAuthnRequestType(UUID.randomUUID().toString(), assertionConsumerURL, this.authServerSamlUrl.toString(), issuer, requestBinding.getBindingUri());
|
||||
if (protocolBinding != null) {
|
||||
loginReq.setProtocolBinding(protocolBinding);
|
||||
}
|
||||
return SAML2Request.convert(loginReq);
|
||||
} catch (ConfigurationException | ParsingException | ProcessingException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
|
|
|
@ -0,0 +1,248 @@
|
|||
package org.keycloak.testsuite.util.saml;
|
||||
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
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.URLEncodedUtils;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.keycloak.common.util.KeyUtils;
|
||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ArtifactResolveType;
|
||||
import org.keycloak.models.SamlArtifactSessionMappingStoreProvider;
|
||||
import org.keycloak.protocol.saml.SamlService;
|
||||
import org.keycloak.protocol.saml.profile.util.Soap;
|
||||
import org.keycloak.saml.BaseSAML2BindingBuilder;
|
||||
import org.keycloak.saml.SignatureAlgorithm;
|
||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||
import org.keycloak.saml.common.util.DocumentUtil;
|
||||
import org.keycloak.saml.common.util.StaxUtil;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
|
||||
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
|
||||
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLRequestWriter;
|
||||
import org.keycloak.testsuite.util.SamlClientBuilder;
|
||||
import org.w3c.dom.Document;
|
||||
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.xml.stream.XMLStreamWriter;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* This builder allows the SamlClient to handle a redirect or a POSTed form which contains an artifact (SAMLart)
|
||||
*/
|
||||
public class HandleArtifactStepBuilder extends SamlDocumentStepBuilder<ArtifactResolveType, HandleArtifactStepBuilder> implements StepWithCheckers {
|
||||
|
||||
private String signingPrivateKeyPem;
|
||||
private String signingPublicKeyPem;
|
||||
private String id = IDGenerator.create("ID_");
|
||||
private String issuer;
|
||||
private final URI authServerSamlUrl;
|
||||
private boolean verifyRedirect;
|
||||
private HttpPost replayPostMessage;
|
||||
private boolean replayPost;
|
||||
private boolean replayArtifact;
|
||||
private AtomicReference<String> providedArtifact;
|
||||
private AtomicReference<String> storeArtifact;
|
||||
|
||||
private Runnable beforeStepChecker;
|
||||
private Runnable afterStepChecker;
|
||||
|
||||
private final static Pattern artifactPattern = Pattern.compile("NAME=\"SAMLart\" VALUE=\"([A-Za-z0-9+=/]*)\"");
|
||||
|
||||
/**
|
||||
* Standard constructor
|
||||
* @param authServerSamlUrl the url of the IdP
|
||||
* @param issuer the value for the issuer
|
||||
* @param clientBuilder the current clientBuilder
|
||||
*/
|
||||
public HandleArtifactStepBuilder(URI authServerSamlUrl, String issuer, SamlClientBuilder clientBuilder) {
|
||||
super(clientBuilder);
|
||||
this.issuer = issuer;
|
||||
this.authServerSamlUrl = authServerSamlUrl.toString().endsWith(SamlService.ARTIFACT_RESOLUTION_SERVICE_PATH) ? authServerSamlUrl : UriBuilder.fromUri(authServerSamlUrl).path(SamlService.ARTIFACT_RESOLUTION_SERVICE_PATH).build();
|
||||
verifyRedirect = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder method. Calling this method with the public and private key will ensure that the generated ArifactResolve is signed
|
||||
* @param signingPrivateKeyPem the pem containing the client's private key
|
||||
* @param signingPublicKeyPem the pem containing the client's public key
|
||||
* @return this HandleArtifactStepBuilder
|
||||
*/
|
||||
public HandleArtifactStepBuilder signWith(String signingPrivateKeyPem, String signingPublicKeyPem) {
|
||||
this.signingPrivateKeyPem = signingPrivateKeyPem;
|
||||
this.signingPublicKeyPem = signingPublicKeyPem;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HandleArtifactStepBuilder issuer(String issuer) {
|
||||
this.issuer = issuer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HandleArtifactStepBuilder setBeforeStepChecks(Runnable checker) {
|
||||
this.beforeStepChecker = checker;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HandleArtifactStepBuilder setAfterStepChecks(Runnable checker) {
|
||||
this.afterStepChecker = checker;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder method. Calling this method with "true" will add an assertion to verify that the returned method was a redirect
|
||||
* @param verifyRedirect set true to verify redirect
|
||||
* @return this HandleArtifactStepBuilder
|
||||
*/
|
||||
public HandleArtifactStepBuilder verifyRedirect(boolean verifyRedirect) {
|
||||
this.verifyRedirect = verifyRedirect;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder method. Call this method with "true" to make sure that the second time "perform" is called, it is a replay of the first time.
|
||||
* This is specifically to test that the artifact is consumed on the IdP side once called.
|
||||
* @param mustReplayPost set true to replay on the second call
|
||||
* @return this HandleArtifactStepBuilder
|
||||
*/
|
||||
public HandleArtifactStepBuilder replayPost(boolean mustReplayPost) {
|
||||
this.replayPost = mustReplayPost;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HandleArtifactStepBuilder storeArtifact(AtomicReference<String> storeArtifact) {
|
||||
this.storeArtifact = storeArtifact;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HandleArtifactStepBuilder useArtifact(AtomicReference<String> artifact) {
|
||||
this.providedArtifact = artifact;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder method. Calling this method will set the ArtifactResolve from the standard generated to a specific id
|
||||
* @param id the value to which to set the ArtifactResolve's id
|
||||
* @return this HandleArtifactStepBuilder
|
||||
*/
|
||||
public HandleArtifactStepBuilder setArtifactResolveId(String id){
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main method. Can read a response with an artifact (redirect or post) and return a POSTed SOAP message containing
|
||||
* the ArtifactResolve message. The behaviour changes depending on what builder methods were called.
|
||||
*
|
||||
* @param client The current http client
|
||||
* @param currentURI the current uri
|
||||
* @param currentResponse the current response from the IdP
|
||||
* @param context the current http context
|
||||
* @return a POSTed SOAP message containing the ArtifactResolve message
|
||||
* @throws Exception
|
||||
*/
|
||||
@Override
|
||||
public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
|
||||
|
||||
if (replayPost && replayPostMessage != null) {
|
||||
return replayPostMessage;
|
||||
}
|
||||
|
||||
ArtifactResolveType artifactResolve = new ArtifactResolveType(id,
|
||||
XMLTimeUtil.getIssueInstant());
|
||||
NameIDType nameIDType = new NameIDType();
|
||||
nameIDType.setValue(issuer);
|
||||
artifactResolve.setIssuer(nameIDType);
|
||||
String artifact = getArtifactFromResponse(currentResponse);
|
||||
if (storeArtifact != null) storeArtifact.set(artifact);
|
||||
artifactResolve.setArtifact(artifact);
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
XMLStreamWriter xmlStreamWriter = StaxUtil.getXMLStreamWriter(bos);
|
||||
new SAMLRequestWriter(xmlStreamWriter).write(artifactResolve);
|
||||
Document doc = DocumentUtil.getDocument(new ByteArrayInputStream(bos.toByteArray()));
|
||||
|
||||
BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder();
|
||||
|
||||
if (signingPrivateKeyPem != null && signingPublicKeyPem != null) {
|
||||
PrivateKey privateKey = org.keycloak.testsuite.util.KeyUtils.privateKeyFromString(signingPrivateKeyPem);
|
||||
PublicKey publicKey = org.keycloak.testsuite.util.KeyUtils.publicKeyFromString(signingPublicKeyPem);
|
||||
binding
|
||||
.signatureAlgorithm(SignatureAlgorithm.RSA_SHA256)
|
||||
.signWith(KeyUtils.createKeyId(privateKey), privateKey, publicKey)
|
||||
.signDocument(doc);
|
||||
}
|
||||
|
||||
String documentAsString = DocumentUtil.getDocumentAsString(doc);
|
||||
String transformed = getTransformer().transform(documentAsString);
|
||||
|
||||
if (transformed == null) return null;
|
||||
|
||||
if (beforeStepChecker != null && beforeStepChecker instanceof SessionStateChecker) {
|
||||
SessionStateChecker sessionStateChecker = (SessionStateChecker) beforeStepChecker;
|
||||
sessionStateChecker.setUserSessionProvider(session -> session.getProvider(SamlArtifactSessionMappingStoreProvider.class).get(artifact).getUserSessionId());
|
||||
sessionStateChecker.setClientSessionProvider(session -> session.getProvider(SamlArtifactSessionMappingStoreProvider.class).get(artifact).getClientSessionId());
|
||||
}
|
||||
|
||||
HttpPost post = Soap.createMessage().addToBody(DocumentUtil.getDocument(transformed)).buildHttpPost(authServerSamlUrl);
|
||||
replayPostMessage = post;
|
||||
return post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the artifact from a response. Can handle both a Redirect and a POSTed form
|
||||
* @param currentResponse the response containing the artifact
|
||||
* @return the artifact
|
||||
* @throws IOException thrown if there'a a problem processing the response.
|
||||
*/
|
||||
private String getArtifactFromResponse(CloseableHttpResponse currentResponse) throws IOException {
|
||||
|
||||
if (providedArtifact != null) {
|
||||
return providedArtifact.get();
|
||||
}
|
||||
|
||||
if (currentResponse.getFirstHeader("location") != null) {
|
||||
String location = currentResponse.getFirstHeader("location").getValue();
|
||||
List<NameValuePair> params = URLEncodedUtils.parse(URI.create(location), Charset.forName("UTF-8"));
|
||||
for (NameValuePair param : params) {
|
||||
if (GeneralConstants.SAML_ARTIFACT_KEY.equals(param.getName())) {
|
||||
String artifact = param.getValue();
|
||||
if (artifact != null && !artifact.isEmpty()) {
|
||||
return artifact;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertFalse(verifyRedirect);
|
||||
String form = EntityUtils.toString(currentResponse.getEntity());
|
||||
|
||||
Matcher m = artifactPattern.matcher(form);
|
||||
assertTrue("Can't find artifact in " + form, m.find());
|
||||
return m.group(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Runnable getBeforeStepChecker() {
|
||||
return beforeStepChecker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Runnable getAfterStepChecker() {
|
||||
return afterStepChecker;
|
||||
}
|
||||
}
|
|
@ -16,6 +16,8 @@
|
|||
*/
|
||||
package org.keycloak.testsuite.util.saml;
|
||||
|
||||
import org.keycloak.saml.common.util.DocumentUtil;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.keycloak.testsuite.util.SamlClientBuilder;
|
||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||
|
@ -81,6 +83,9 @@ public class ModifySamlResponseStepBuilder extends SamlDocumentStepBuilder<SAML2
|
|||
|
||||
case POST:
|
||||
return handlePostBinding(currentResponse);
|
||||
|
||||
case ARTIFACT_RESPONSE:
|
||||
return handleArtifactResponse(currentResponse);
|
||||
}
|
||||
|
||||
throw new RuntimeException("Unknown binding for " + ModifySamlResponseStepBuilder.class.getName());
|
||||
|
@ -130,6 +135,18 @@ public class ModifySamlResponseStepBuilder extends SamlDocumentStepBuilder<SAML2
|
|||
return this;
|
||||
}
|
||||
|
||||
private HttpUriRequest handleArtifactResponse(CloseableHttpResponse currentResponse) throws Exception {
|
||||
SAMLDocumentHolder samlDocumentHolder = null;
|
||||
try {
|
||||
samlDocumentHolder = Binding.ARTIFACT_RESPONSE.extractResponse(currentResponse);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
return createRequest(this.targetUri, this.targetAttribute, DocumentUtil.asString(samlDocumentHolder.getSamlDocument()), new LinkedList<>());
|
||||
}
|
||||
|
||||
protected HttpUriRequest handleRedirectBinding(CloseableHttpResponse currentResponse) throws Exception, IOException, URISyntaxException {
|
||||
String samlDoc;
|
||||
final String attrName;
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
package org.keycloak.testsuite.util.saml;
|
||||
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.keycloak.saml.SAMLRequestParser;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.keycloak.saml.processing.web.util.RedirectBindingUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class SamlMessageReceiver implements AutoCloseable {
|
||||
|
||||
private final static Pattern SAML_MESSAGE_PATTER = Pattern.compile(".*SAML(?:Response|Request)=([^&]*).*");
|
||||
|
||||
private final HttpServer server;
|
||||
private String message;
|
||||
private final String url;
|
||||
|
||||
public SamlMessageReceiver(int port) {
|
||||
try {
|
||||
InetSocketAddress address = new InetSocketAddress(InetAddress.getByName("localhost"), port);
|
||||
server = HttpServer.create(address, 0);
|
||||
this.url = "http://" + address.getHostString() + ":" + port ;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Cannot create http server", e);
|
||||
}
|
||||
|
||||
server.createContext("/", new MyHandler());
|
||||
server.setExecutor(null);
|
||||
server.start();
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public boolean isMessageReceived() {
|
||||
return message != null && !message.trim().isEmpty();
|
||||
}
|
||||
|
||||
public String getMessageString() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public SAMLDocumentHolder getSamlDocumentHolder() {
|
||||
Matcher m = SAML_MESSAGE_PATTER.matcher(message);
|
||||
if (m.find()) {
|
||||
try {
|
||||
return SAMLRequestParser.parseResponsePostBinding(RedirectBindingUtil.urlDecode(m.group(1)));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Cannot parse response " + m.group(1), e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
server.stop(0);
|
||||
}
|
||||
|
||||
private class MyHandler implements HttpHandler {
|
||||
public void handle(HttpExchange t) throws IOException {
|
||||
t.sendResponseHeaders(200, 0);
|
||||
|
||||
SamlMessageReceiver.this.message = IOUtils.toString(t.getRequestBody(), StandardCharsets.UTF_8.name());
|
||||
|
||||
OutputStream os = t.getResponseBody();
|
||||
os.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
package org.keycloak.testsuite.util.saml;
|
||||
|
||||
import org.infinispan.util.function.SerializableConsumer;
|
||||
import org.infinispan.util.function.SerializableFunction;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.sessions.CommonClientSessionModel;
|
||||
import org.keycloak.testsuite.client.KeycloakTestingClient;
|
||||
import org.keycloak.testsuite.runonserver.FetchOnServer;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
public class SessionStateChecker implements Runnable {
|
||||
|
||||
private String realmName = "demo";
|
||||
private AtomicReference<String> userSessionIdStore;
|
||||
private AtomicReference<String> expectedUserSession;
|
||||
private String expectedClientSession;
|
||||
private SerializableConsumer<UserSessionModel> consumeUserSession;
|
||||
private final Map<String, SerializableConsumer<AuthenticatedClientSessionModel>> consumeClientSession = new HashMap<>();
|
||||
|
||||
private SerializableFunction<KeycloakSession, String> userSessionIdProvider;
|
||||
private SerializableFunction<KeycloakSession, String> clientSessionIdProvider;
|
||||
private final KeycloakTestingClient.Server server;
|
||||
|
||||
|
||||
public SessionStateChecker(KeycloakTestingClient.Server server) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
public SessionStateChecker realmName(String realmName) {
|
||||
this.realmName = realmName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SessionStateChecker setUserSessionProvider(SerializableFunction<KeycloakSession, String> sessionProvider) {
|
||||
this.userSessionIdProvider = sessionProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SessionStateChecker setClientSessionProvider(SerializableFunction<KeycloakSession, String> sessionProvider) {
|
||||
this.clientSessionIdProvider = sessionProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SessionStateChecker storeUserSessionId(AtomicReference<String> userSessionIdStore) {
|
||||
this.userSessionIdStore = userSessionIdStore;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SessionStateChecker consumeClientSession(String clientSessionId, SerializableConsumer<AuthenticatedClientSessionModel> consumer) {
|
||||
consumeClientSession.merge(clientSessionId, consumer, (consumer1, consumer2) -> {
|
||||
return (SerializableConsumer<AuthenticatedClientSessionModel>) clientSessionModel -> {
|
||||
consumer1.accept(clientSessionModel);
|
||||
consumer2.accept(clientSessionModel);
|
||||
};
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public SessionStateChecker consumeUserSession(SerializableConsumer<UserSessionModel> userSessionModelConsumer) {
|
||||
if (consumeUserSession == null) {
|
||||
consumeUserSession = userSessionModelConsumer;
|
||||
} else {
|
||||
consumeUserSession = mergeConsumers(consumeUserSession, userSessionModelConsumer);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public SerializableConsumer<UserSessionModel> mergeConsumers(SerializableConsumer<UserSessionModel> consumer1, SerializableConsumer<UserSessionModel> consumer2) {
|
||||
return userSessionModel -> {
|
||||
consumer1.accept(userSessionModel);
|
||||
consumer2.accept(userSessionModel);
|
||||
};
|
||||
}
|
||||
|
||||
public SessionStateChecker expectedAction(String clientId, CommonClientSessionModel.Action action) {
|
||||
consumeClientSession(clientId, clientSessionModel -> {
|
||||
if (action == null) {
|
||||
assertThat(clientSessionModel, notNullValue());
|
||||
assertThat(clientSessionModel.getAction(), nullValue());
|
||||
return;
|
||||
}
|
||||
assertThat(clientSessionModel, notNullValue());
|
||||
assertThat(clientSessionModel.getAction(), equalTo(action.name()));
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public SessionStateChecker expectedState(UserSessionModel.State state) {
|
||||
consumeUserSession(userSessionModel -> {
|
||||
assertThat(userSessionModel, notNullValue());
|
||||
assertThat(userSessionModel.getState(), equalTo(state));
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public SessionStateChecker expectedNumberOfClientSessions(int expectedNumberOfClientSession) {
|
||||
consumeUserSession(userSession -> assertThat(userSession.getAuthenticatedClientSessions().keySet(), hasSize(expectedNumberOfClientSession)));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public SessionStateChecker expectedUserSession(AtomicReference<String> expectedUserSession) {
|
||||
this.expectedUserSession = expectedUserSession;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SessionStateChecker expectedClientSession(String expectedClientSession) {
|
||||
this.expectedClientSession = expectedClientSession;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
run(server, realmName, userSessionIdStore, expectedUserSession, expectedClientSession, consumeUserSession, consumeClientSession, userSessionIdProvider, clientSessionIdProvider);
|
||||
}
|
||||
|
||||
public static void run(KeycloakTestingClient.Server server,
|
||||
String realmName,
|
||||
AtomicReference<String> userSessionIdStore,
|
||||
AtomicReference<String> expectedUserSession,
|
||||
String expectedClientSession,
|
||||
SerializableConsumer<UserSessionModel> consumeUserSession,
|
||||
Map<String, SerializableConsumer<AuthenticatedClientSessionModel>> consumeClientSession,
|
||||
SerializableFunction<KeycloakSession, String> userSessionIdProvider,
|
||||
SerializableFunction<KeycloakSession, String> clientSessionIdProvider) {
|
||||
if (server == null || userSessionIdProvider == null)
|
||||
throw new RuntimeException("Wrongly configured session checker");
|
||||
|
||||
if (userSessionIdStore != null) {
|
||||
String userSession = server.fetchString((FetchOnServer) userSessionIdProvider::apply);
|
||||
userSessionIdStore.set(userSession.replace("\"", ""));
|
||||
}
|
||||
|
||||
server.run(session -> {
|
||||
String sessionId = userSessionIdProvider.apply(session);
|
||||
|
||||
if (expectedUserSession != null) {
|
||||
assertThat(sessionId, equalTo(expectedUserSession.get()));
|
||||
}
|
||||
|
||||
if (expectedClientSession != null) {
|
||||
String clientSession = clientSessionIdProvider.apply(session);
|
||||
assertThat(clientSession, equalTo(expectedClientSession));
|
||||
}
|
||||
|
||||
RealmModel realm = session.realms().getRealmByName(realmName);
|
||||
UserSessionModel userSessionModel = session.sessions().getUserSession(realm, sessionId);
|
||||
if (consumeUserSession != null) consumeUserSession.accept(userSessionModel);
|
||||
|
||||
if (!consumeClientSession.isEmpty()) {
|
||||
consumeClientSession.forEach((id, consumer) -> consumer.accept(userSessionModel.getAuthenticatedClientSessionByClient(id)));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package org.keycloak.testsuite.util.saml;
|
||||
|
||||
public interface StepWithCheckers {
|
||||
default Runnable getBeforeStepChecker() {
|
||||
return null;
|
||||
}
|
||||
default Runnable getAfterStepChecker() {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -191,13 +191,13 @@ public abstract class AbstractKeycloakTest {
|
|||
|
||||
protected void beforeAbstractKeycloakTestRealmImport() throws Exception {
|
||||
}
|
||||
protected void postAfterAbstractKeycloak() {
|
||||
protected void postAfterAbstractKeycloak() throws Exception {
|
||||
}
|
||||
|
||||
protected void afterAbstractKeycloakTestRealmImport() {}
|
||||
|
||||
@After
|
||||
public void afterAbstractKeycloakTest() {
|
||||
public void afterAbstractKeycloakTest() throws Exception {
|
||||
if (resetTimeOffset) {
|
||||
resetTimeOffset();
|
||||
}
|
||||
|
|
|
@ -582,8 +582,9 @@ public class RealmTest extends AbstractAdminTest {
|
|||
|
||||
ClientRepresentation converted = realm.convertClientDescription(description);
|
||||
assertEquals("loadbalancer-9.siroe.com", converted.getClientId());
|
||||
assertEquals(1, converted.getRedirectUris().size());
|
||||
assertEquals(2, converted.getRedirectUris().size());
|
||||
assertEquals("https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp", converted.getRedirectUris().get(0));
|
||||
assertEquals("https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp", converted.getRedirectUris().get(1));
|
||||
}
|
||||
|
||||
public static void assertRealm(RealmRepresentation realm, RealmRepresentation storedRealm) {
|
||||
|
|
|
@ -80,8 +80,9 @@ public class SAMLClientRegistrationTest extends AbstractClientRegistrationTest {
|
|||
"https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/post",
|
||||
"https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/soap",
|
||||
"https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/paos",
|
||||
"https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/redirect"
|
||||
)); // No redirect URI for ARTIFACT binding which is unsupported
|
||||
"https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/redirect",
|
||||
"https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/artifact"
|
||||
));
|
||||
|
||||
assertThat(response.getAttributes().get("saml_single_logout_service_url_redirect"), is("https://LoadBalancer-9.siroe.com:3443/federation/SPSloRedirect/metaAlias/sp"));
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
|
|||
|
||||
@After
|
||||
@Override
|
||||
public void afterAbstractKeycloakTest() {
|
||||
public void afterAbstractKeycloakTest() throws Exception {
|
||||
log.debug("--DC: after AbstractCrossDCTest");
|
||||
CrossDCTestEnricher.startAuthServerBackendNode(DC.FIRST, 0); // make sure first node is started
|
||||
enableOnlyFirstNodeInFirstDc();
|
||||
|
|
|
@ -128,7 +128,7 @@ public class DockerClientTest extends AbstractKeycloakTest {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void afterAbstractKeycloakTest() {
|
||||
public void afterAbstractKeycloakTest() throws Exception {
|
||||
super.afterAbstractKeycloakTest();
|
||||
|
||||
pause(5000); // wait for the container logs
|
||||
|
|
|
@ -162,7 +162,7 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
|
|||
|
||||
@After
|
||||
@Override
|
||||
public void afterAbstractKeycloakTest() {
|
||||
public void afterAbstractKeycloakTest() throws Exception {
|
||||
cleanupApacheHttpClient();
|
||||
|
||||
super.afterAbstractKeycloakTest();
|
||||
|
|
|
@ -80,7 +80,7 @@ public class OAuthRedirectUriTest extends AbstractKeycloakTest {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void afterAbstractKeycloakTest() {
|
||||
public void afterAbstractKeycloakTest() throws Exception {
|
||||
super.afterAbstractKeycloakTest();
|
||||
|
||||
server.stop(0);
|
||||
|
|
|
@ -8,13 +8,13 @@ import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
|||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.protocol.saml.SamlProtocol;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.testsuite.AbstractAuthTest;
|
||||
import org.keycloak.testsuite.util.SamlClient;
|
||||
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriBuilderException;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import java.net.URI;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PrivateKey;
|
||||
|
@ -52,9 +52,13 @@ public abstract class AbstractSamlTest extends AbstractAuthTest {
|
|||
public static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST2 = AUTH_SERVER_SCHEME + "://localhost:" + (AUTH_SERVER_SSL_REQUIRED ? AUTH_SERVER_PORT : 8080) + "/sales-post2/saml";
|
||||
public static final String SAML_CLIENT_ID_SALES_POST2 = "http://localhost:8280/sales-post2/";
|
||||
|
||||
public static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST_SIG = AUTH_SERVER_SCHEME + "://localhost:" + (AUTH_SERVER_SSL_REQUIRED ? AUTH_SERVER_PORT : 8080) + "/sales-post-sig/";
|
||||
public static final String SAML_CLIENT_ID_SALES_POST_SIG = "http://localhost:8280/sales-post-sig/";
|
||||
public static final String SAML_URL_SALES_POST_SIG = "http://localhost:8080/sales-post-sig/";
|
||||
public static final String SAML_CLIENT_ID_SALES_POST_SIG = "http://localhost:8280/sales-post-sig/";
|
||||
public static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST_SIG = AUTH_SERVER_SCHEME + "://localhost:" + (AUTH_SERVER_SSL_REQUIRED ? AUTH_SERVER_PORT : 8080) + "/sales-post-sig/";
|
||||
|
||||
public static final String SAML_CLIENT_ID_SALES_POST_ASSERTION_AND_RESPONSE_SIG = "http://localhost:8280/sales-post-assertion-and-response-sig/";
|
||||
public static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST_ASSERTION_AND_RESPONSE_SIG = AUTH_SERVER_SCHEME + "://localhost:" + (AUTH_SERVER_SSL_REQUIRED ? AUTH_SERVER_PORT : 8080) + "/sales-post-assertion-and-response-sig/";
|
||||
|
||||
public static final String SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBANUbxrvEY3pkiQNt55zJLKBwN+zKmNQw08ThAmOKzwHfXoK+xlDSFxNMtTKJGkeUdnKzaTfESEcEfKYULUA41y/NnOlvjS0CEsc7Wq0Ce63TSSGMB2NHea4tV0aQz/MwLsbmz2IjAFWHA5CHL5WwacIf3UTOSNnhJUSvnkomjJAlAgMBAAECgYANpO2gb/5+g5lSIuNFYov86bJq8r2+ODIW1OE2Rljioc6HSHeiDRF1JuAjECwikRrUVTBTZbnK8jqY14neJsWAKBzGo+ToaQALsNZ9B91DxxL50K5oVOzw5shAS9TnRjN40+KIXFED4ydq4JRdoqb8+cN+N3i0+Cu7tdm+UaHTAQJBAOwFs3ZwqQEqmv9vmgmIFwFpJm1aIw25gEOf3Hy45GP4bL/j0FQgwcXYRbLE5bPqhw/liLKc1GQ97bVm6zs8SvUCQQDnJZA6TFRMiDjezinE1J4e0v4RupyDniVjbE5ArTK5/FRVkjw4Ny0AqZUEyIIqlTeZlCq45pCJy4a2hymDGVJxAj9gzfXNnmezEsZ//kYvoqHM8lPQhifaeTsigW7tuOf0GPCBw+6uksDnZM0xhZCxOoArBPoMSEbU1pGo1Y2lvhUCQF6E5sBgHAybm53Ich4Rz4LNRqWbSIstrR5F2I3sBRU2kInZXZSjQ1zE+7HUCB4/nFfJ1dp8NdiTCEg1Zw072pECQQDnxyQALmWhQbBTl0tq6CwYf9rZDwBzxuY+CXB8Ky1gOmXwan96KZvV4rK8MQQs6HIiYC/j+5lX3A3zlXTFldaz";
|
||||
public static final String SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVG8a7xGN6ZIkDbeecySygcDfsypjUMNPE4QJjis8B316CvsZQ0hcTTLUyiRpHlHZys2k3xEhHBHymFC1AONcvzZzpb40tAhLHO1qtAnut00khjAdjR3muLVdGkM/zMC7G5s9iIwBVhwOQhy+VsGnCH91EzkjZ4SVEr55KJoyQJQIDAQAB";
|
||||
public static final PrivateKey SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY_PK;
|
||||
|
@ -76,7 +80,7 @@ public abstract class AbstractSamlTest extends AbstractAuthTest {
|
|||
public static final String SAML_CLIENT_SALES_POST_SIG_EXPIRED_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDKxs0adx1X+k4u+a5eZjwD17mvADwgiwDYpMznfNlSNEfDJdFAHIZH0VAbwXnaGySJ/a/MMMTHly5irDMp1udkmHgv2ceW+SumsjEtxliSIKi6af59aYlHiOLGyV5VI/VLVvkE6Roax7fZ+7O858KDahg1JI5smYnpBLKY3X885QIDAQAB";
|
||||
public static final String SAML_CLIENT_SALES_POST_SIG_EXPIRED_CERTIFICATE = "MIICMTCCAZqgAwIBAgIJAPlizW20Nhe6MA0GCSqGSIb3DQEBCwUAMDAxLjAsBgNVBAMMJWh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYWxlcy1wb3N0LXNpZy8wHhcNMTYwODI5MDg1MjMzWhcNMTYwODMwMDg1MjMzWjAwMS4wLAYDVQQDDCVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDKxs0adx1X+k4u+a5eZjwD17mvADwgiwDYpMznfNlSNEfDJdFAHIZH0VAbwXnaGySJ/a/MMMTHly5irDMp1udkmHgv2ceW+SumsjEtxliSIKi6af59aYlHiOLGyV5VI/VLVvkE6Roax7fZ+7O858KDahg1JI5smYnpBLKY3X885QIDAQABo1MwUTAdBgNVHQ4EFgQUE9C6Ck0jsdY+sjN064ZYwYkZJr4wHwYDVR0jBBgwFoAUE9C6Ck0jsdY+sjN064ZYwYkZJr4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQBuypHw5DMDBgfI6LcXBiCjpiQP3DLRLdwthh/RfCnZT7PrhXRJV8RMm8EqxqtEgfg2SKqMyA02uxMKH0p277U2iQveSDAaICTJRxtyFm6FERtgLNlsekusC2I14gZpLe84oHDf6L1w3dKFzzLEC9+bHg/XCg/KthWxW8iuVct5qg==";
|
||||
|
||||
public static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST_ENC = "http://localhost:8080/sales-post-enc/";
|
||||
public static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST_ENC = AUTH_SERVER_SCHEME + "://localhost:" + (AUTH_SERVER_SSL_REQUIRED ? AUTH_SERVER_PORT : 8080) + "/sales-post-enc/saml";
|
||||
public static final String SAML_CLIENT_ID_SALES_POST_ENC = "http://localhost:8280/sales-post-enc/";
|
||||
public static final String SAML_CLIENT_SALES_POST_ENC_PRIVATE_KEY = "MIICXQIBAAKBgQDb7kwJPkGdU34hicplwfp6/WmNcaLh94TSc7Jyr9Undp5pkyLgb0DE7EIE+6kSs4LsqCb8HDkB0nLD5DXbBJFd8n0WGoKstelvtg6FtVJMnwN7k7yZbfkPECWH9zF70VeOo9vbzrApNRnct8ZhH5fbflRB4JMA9L9R+LbURdoSKQIDAQABAoGBANtbZG9bruoSGp2s5zhzLzd4hczT6Jfk3o9hYjzNb5Z60ymN3Z1omXtQAdEiiNHkRdNxK+EM7TcKBfmoJqcaeTkW8cksVEAW23ip8W9/XsLqmbU2mRrJiKa+KQNDSHqJi1VGyimi4DDApcaqRZcaKDFXg2KDr/Qt5JFD/o9IIIPZAkEA+ZENdBIlpbUfkJh6Ln+bUTss/FZ1FsrcPZWu13rChRMrsmXsfzu9kZUWdUeQ2Dj5AoW2Q7L/cqdGXS7Mm5XhcwJBAOGZq9axJY5YhKrsksvYRLhQbStmGu5LG75suF+rc/44sFq+aQM7+oeRr4VY88Mvz7mk4esdfnk7ae+cCazqJvMCQQCx1L1cZw3yfRSn6S6u8XjQMjWE/WpjulujeoRiwPPY9WcesOgLZZtYIH8nRL6ehEJTnMnahbLmlPFbttxPRUanAkA11MtSIVcKzkhp2KV2ipZrPJWwI18NuVJXb+3WtjypTrGWFZVNNkSjkLnHIeCYlJIGhDd8OL9zAiBXEm6kmgLNAkBWAg0tK2hCjvzsaA505gWQb4X56uKWdb0IzN+fOLB3Qt7+fLqbVQNQoNGzqey6B4MoS1fUKAStqdGTFYPG/+9t";
|
||||
public static final String SAML_CLIENT_SALES_POST_ENC_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDb7kwJPkGdU34hicplwfp6/WmNcaLh94TSc7Jyr9Undp5pkyLgb0DE7EIE+6kSs4LsqCb8HDkB0nLD5DXbBJFd8n0WGoKstelvtg6FtVJMnwN7k7yZbfkPECWH9zF70VeOo9vbzrApNRnct8ZhH5fbflRB4JMA9L9R+LbURdoSKQIDAQAB";
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
package org.keycloak.testsuite.saml;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
|
||||
import org.keycloak.testsuite.authentication.CustomTestingSamlArtifactResolver;
|
||||
import org.keycloak.testsuite.util.ContainerAssume;
|
||||
import org.keycloak.testsuite.util.SamlClient;
|
||||
import org.keycloak.testsuite.util.SamlClientBuilder;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.keycloak.testsuite.util.SamlClient.Binding.POST;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
@AuthServerContainerExclude({AuthServerContainerExclude.AuthServer.QUARKUS}) // Can't be done on quarkus because currently quarkus doesn't support the SetDefaultProvider annotation
|
||||
@SetDefaultProvider(spi = "saml-artifact-resolver", providerId = "0005")
|
||||
public class ArtifactBindingCustomResolverTest extends ArtifactBindingTest {
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
@Override
|
||||
public void testArtifactBindingLogoutSingleClientCheckArtifact() {}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
@Override
|
||||
public void testArtifactBindingLoginCheckArtifactWithPost() {}
|
||||
|
||||
@Test
|
||||
public void testCustomArtifact() {
|
||||
AtomicReference<String> artifactReference = new AtomicReference<>();
|
||||
|
||||
new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.POST)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
|
||||
.build()
|
||||
.login().user(bburkeUser).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST)
|
||||
.storeArtifact(artifactReference)
|
||||
.build()
|
||||
.execute();
|
||||
|
||||
String artifact = artifactReference.get();
|
||||
byte[] byteArray = Base64.getDecoder().decode(artifact);
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(byteArray);
|
||||
bis.skip(2);
|
||||
int index = bis.read();
|
||||
|
||||
assertThat(byteArray[0], is((byte)0));
|
||||
assertThat(byteArray[1], is((byte)5));
|
||||
|
||||
if (!suiteContext.getAuthServerInfo().isUndertow()) return;
|
||||
|
||||
String storedResponse = CustomTestingSamlArtifactResolver.list.get(index);
|
||||
|
||||
assertThat(storedResponse, notNullValue());
|
||||
assertThat(storedResponse, containsString("samlp:Response"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactDoesntContainSignature() {
|
||||
ContainerAssume.assumeAuthServerUndertow();
|
||||
|
||||
AtomicReference<String> artifactReference = new AtomicReference<>();
|
||||
|
||||
new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_ASSERTION_AND_RESPONSE_SIG,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST_ASSERTION_AND_RESPONSE_SIG, POST)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
|
||||
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||
.build()
|
||||
.login().user(bburkeUser).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_ASSERTION_AND_RESPONSE_SIG)
|
||||
.storeArtifact(artifactReference)
|
||||
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||
.build()
|
||||
.execute();
|
||||
|
||||
String artifact = artifactReference.get();
|
||||
byte[] byteArray = Base64.getDecoder().decode(artifact);
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(byteArray);
|
||||
bis.skip(2);
|
||||
int index = bis.read();
|
||||
|
||||
assertThat(byteArray[0], is((byte)0));
|
||||
assertThat(byteArray[1], is((byte)5));
|
||||
|
||||
String storedResponse = CustomTestingSamlArtifactResolver.list.get(index);
|
||||
|
||||
assertThat(storedResponse, notNullValue());
|
||||
assertThat(storedResponse, containsString("samlp:Response"));
|
||||
assertThat(storedResponse, not(containsString("Signature")));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,987 @@
|
|||
package org.keycloak.testsuite.saml;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.adapters.saml.SamlDeployment;
|
||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||
import org.keycloak.dom.saml.v2.metadata.SPSSODescriptorType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
|
||||
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||
import org.keycloak.dom.saml.v2.protocol.NameIDMappingResponseType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
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.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.saml.SAML2LogoutResponseBuilder;
|
||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
|
||||
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
|
||||
import org.keycloak.sessions.CommonClientSessionModel;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
|
||||
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
|
||||
import org.keycloak.testsuite.util.SamlClient;
|
||||
import org.keycloak.testsuite.util.SamlClientBuilder;
|
||||
import org.keycloak.testsuite.util.SamlUtils;
|
||||
import org.keycloak.testsuite.util.saml.HandleArtifactStepBuilder;
|
||||
import org.keycloak.testsuite.util.saml.SamlMessageReceiver;
|
||||
import org.keycloak.testsuite.util.saml.SessionStateChecker;
|
||||
import org.keycloak.testsuite.utils.io.IOUtil;
|
||||
import org.w3c.dom.Document;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.isEmptyOrNullString;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.keycloak.testsuite.util.Matchers.bodyHC;
|
||||
import static org.keycloak.testsuite.util.Matchers.isSamlLogoutRequest;
|
||||
import static org.keycloak.testsuite.util.Matchers.isSamlResponse;
|
||||
import static org.keycloak.testsuite.util.Matchers.isSamlStatusResponse;
|
||||
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
|
||||
import static org.keycloak.testsuite.util.SamlClient.Binding.ARTIFACT_RESPONSE;
|
||||
import static org.keycloak.testsuite.util.SamlClient.Binding.POST;
|
||||
import static org.keycloak.testsuite.util.SamlClient.Binding.REDIRECT;
|
||||
import static org.keycloak.testsuite.util.SamlUtils.getSPInstallationDescriptor;
|
||||
|
||||
public class ArtifactBindingTest extends AbstractSamlTest {
|
||||
|
||||
@Rule
|
||||
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
|
||||
|
||||
/************************ LOGIN TESTS ************************/
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingTimesOutAfterCodeToTokenLifespan() throws Exception {
|
||||
|
||||
getCleanup().addCleanup(
|
||||
new RealmAttributeUpdater(adminClient.realm(REALM_NAME))
|
||||
.setAccessCodeLifespan(1)
|
||||
.update()
|
||||
);
|
||||
|
||||
SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.POST)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
|
||||
.build()
|
||||
.login().user(bburkeUser).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST)
|
||||
.setBeforeStepChecks(() -> setTimeOffset(1000)) // Move in time before resolving the artifact
|
||||
.build()
|
||||
.doNotFollowRedirects()
|
||||
.executeAndTransform(this::getArtifactResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
|
||||
ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
|
||||
assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(artifactResponse.getAny(), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingWithResponseAndAssertionSignature() throws Exception {
|
||||
SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_ASSERTION_AND_RESPONSE_SIG,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST_ASSERTION_AND_RESPONSE_SIG, POST)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
|
||||
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||
.build()
|
||||
.login()
|
||||
.user(bburkeUser)
|
||||
.build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_ASSERTION_AND_RESPONSE_SIG)
|
||||
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||
.build()
|
||||
.doNotFollowRedirects()
|
||||
.executeAndTransform(this::getArtifactResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
|
||||
ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
|
||||
assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(artifactResponse.getAny(), instanceOf(ResponseType.class));
|
||||
ResponseType samlResponse = (ResponseType)artifactResponse.getAny();
|
||||
assertThat(samlResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(samlResponse.getAssertions().get(0).getAssertion().getSignature(), not(nullValue()));
|
||||
|
||||
SamlDeployment deployment = SamlUtils.getSamlDeploymentForClient("sales-post-assertion-and-response-sig");
|
||||
SamlProtocolUtils.verifyDocumentSignature(response.getSamlDocument(), deployment.getIDP().getSignatureValidationKeyLocator()); // Checks the signature of the response as well as the signature of the assertion
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingWithEncryptedAssertion() throws Exception {
|
||||
SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_ENC,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST_ENC, POST)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
|
||||
.signWith(SAML_CLIENT_SALES_POST_ENC_PRIVATE_KEY, SAML_CLIENT_SALES_POST_ENC_PUBLIC_KEY)
|
||||
.build()
|
||||
.login().user(bburkeUser).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_ENC)
|
||||
.signWith(SAML_CLIENT_SALES_POST_ENC_PRIVATE_KEY, SAML_CLIENT_SALES_POST_ENC_PUBLIC_KEY)
|
||||
.build()
|
||||
.doNotFollowRedirects()
|
||||
.executeAndTransform(ARTIFACT_RESPONSE::extractResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(ResponseType.class));
|
||||
ResponseType loginResponse = (ResponseType) response.getSamlObject();
|
||||
assertThat(loginResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(loginResponse.getAssertions().get(0).getAssertion(), nullValue());
|
||||
assertThat(loginResponse.getAssertions().get(0).getEncryptedAssertion(), not(nullValue()));
|
||||
|
||||
SamlDeployment deployment = SamlUtils.getSamlDeploymentForClient("sales-post-enc");
|
||||
AssertionUtil.decryptAssertion(response, loginResponse, deployment.getDecryptionKey());
|
||||
|
||||
assertThat(loginResponse.getAssertions().get(0).getAssertion(), not(nullValue()));
|
||||
assertThat(loginResponse.getAssertions().get(0).getEncryptedAssertion(), nullValue());
|
||||
assertThat(loginResponse.getAssertions().get(0).getAssertion().getIssuer().getValue(), equalTo(getAuthServerRealmBase(REALM_NAME).toString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingLoginCheckArtifactWithPost() throws NoSuchAlgorithmException {
|
||||
String response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.POST)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
|
||||
.build()
|
||||
.login().user(bburkeUser).build().doNotFollowRedirects().executeAndTransform(resp -> EntityUtils.toString(resp.getEntity()));
|
||||
assertThat(response, containsString(GeneralConstants.SAML_ARTIFACT_KEY));
|
||||
|
||||
Pattern artifactPattern = Pattern.compile("NAME=\"SAMLart\" VALUE=\"((?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=))");
|
||||
Matcher m = artifactPattern.matcher(response);
|
||||
assertThat(m.find(), is(true));
|
||||
|
||||
String artifactB64 = m.group(1);
|
||||
assertThat(artifactB64,not(isEmptyOrNullString()));
|
||||
|
||||
byte[] artifact = Base64.getDecoder().decode(artifactB64);
|
||||
assertThat(artifact.length, is(44));
|
||||
assertThat(artifact[0], is((byte)0));
|
||||
assertThat(artifact[1], is((byte)4));
|
||||
assertThat(artifact[2], is((byte)0));
|
||||
assertThat(artifact[3], is((byte)0));
|
||||
|
||||
MessageDigest sha1Digester = MessageDigest.getInstance("SHA-1");
|
||||
byte[] source = sha1Digester.digest(getAuthServerRealmBase(REALM_NAME).toString().getBytes(Charsets.UTF_8));
|
||||
for (int i = 0; i < 20; i++) {
|
||||
assertThat(source[i], is(artifact[i+4]));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingLoginFullExchangeWithPost() {
|
||||
SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.POST)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
|
||||
.build()
|
||||
.login().user(bburkeUser).build().handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).build()
|
||||
.doNotFollowRedirects().executeAndTransform(this::getArtifactResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
|
||||
ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
|
||||
assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(artifactResponse.getSignature(), nullValue());
|
||||
assertThat(artifactResponse.getAny(), instanceOf(ResponseType.class));
|
||||
assertThat(artifactResponse.getInResponseTo(), not(isEmptyOrNullString()));
|
||||
ResponseType samlResponse = (ResponseType)artifactResponse.getAny();
|
||||
assertThat(samlResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingLoginCorrectSignature() {
|
||||
SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_SIG,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST_SIG, SamlClient.Binding.POST)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
|
||||
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY
|
||||
, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||
.build()
|
||||
.login().user(bburkeUser).build().handleArtifact(getAuthServerSamlEndpoint(REALM_NAME)
|
||||
, SAML_CLIENT_ID_SALES_POST_SIG).signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY
|
||||
, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY).build()
|
||||
.doNotFollowRedirects().executeAndTransform(this::getArtifactResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
|
||||
ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
|
||||
assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(artifactResponse.getAny(), instanceOf(ResponseType.class));
|
||||
assertThat(artifactResponse.getSignature(), not(nullValue()));
|
||||
|
||||
ResponseType samlResponse = (ResponseType)artifactResponse.getAny();
|
||||
assertThat(samlResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(samlResponse.getAssertions().get(0).getAssertion().getSignature(), nullValue());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingLoginIncorrectSignature() {
|
||||
SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_SIG,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST_SIG, SamlClient.Binding.POST)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
|
||||
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY
|
||||
, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||
.build()
|
||||
.login().user(bburkeUser).build()
|
||||
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME)
|
||||
, SAML_CLIENT_ID_SALES_POST_SIG).signWith(SAML_CLIENT_SALES_POST_SIG_EXPIRED_PRIVATE_KEY,
|
||||
SAML_CLIENT_SALES_POST_SIG_EXPIRED_PUBLIC_KEY)
|
||||
.build()
|
||||
.doNotFollowRedirects()
|
||||
.executeAndTransform(this::getArtifactResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
|
||||
ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
|
||||
assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(artifactResponse.getAny(), nullValue());
|
||||
assertThat(artifactResponse.getSignature(), not(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingLoginGetArtifactResponseTwice() {
|
||||
SamlClientBuilder clientBuilder = new SamlClientBuilder();
|
||||
HandleArtifactStepBuilder handleArtifactBuilder = new HandleArtifactStepBuilder(
|
||||
getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, clientBuilder);
|
||||
|
||||
SAMLDocumentHolder response= clientBuilder.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.REDIRECT)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
|
||||
.build()
|
||||
.login().user(bburkeUser).build()
|
||||
.handleArtifact(handleArtifactBuilder).build()
|
||||
.processSamlResponse(ARTIFACT_RESPONSE)
|
||||
.transformObject(ob -> {
|
||||
assertThat(ob, isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
return null;
|
||||
})
|
||||
.build()
|
||||
.handleArtifact(handleArtifactBuilder).replayPost(true).build()
|
||||
.doNotFollowRedirects()
|
||||
.executeAndTransform(this::getArtifactResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
|
||||
ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
|
||||
assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(artifactResponse.getAny(), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactSuccessfulAfterFirstUnsuccessfulRequest() {
|
||||
SamlClientBuilder clientBuilder = new SamlClientBuilder();
|
||||
|
||||
AtomicReference<String> artifact = new AtomicReference<>();
|
||||
|
||||
SAMLDocumentHolder response = clientBuilder.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.POST)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
|
||||
.build()
|
||||
.login().user(bburkeUser).build()
|
||||
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2) // Wrong issuer
|
||||
.storeArtifact(artifact)
|
||||
.build()
|
||||
.assertResponse(r -> assertThat(r, bodyHC(containsString(JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get()))))
|
||||
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST)
|
||||
.useArtifact(artifact)
|
||||
.build()
|
||||
.executeAndTransform(ARTIFACT_RESPONSE::extractResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingLoginForceArtifactBinding() {
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
|
||||
.update()
|
||||
);
|
||||
|
||||
SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.POST)
|
||||
.build()
|
||||
.login().user(bburkeUser).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).build()
|
||||
.doNotFollowRedirects()
|
||||
.executeAndTransform(this::getArtifactResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
|
||||
ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
|
||||
assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(artifactResponse.getAny(), instanceOf(ResponseType.class));
|
||||
ResponseType samlResponse = (ResponseType)artifactResponse.getAny();
|
||||
assertThat(samlResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingLoginSignedArtifactResponse() throws Exception {
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
|
||||
.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "true")
|
||||
.update()
|
||||
);
|
||||
|
||||
SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.POST)
|
||||
.build()
|
||||
.login().user(bburkeUser).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).build()
|
||||
.doNotFollowRedirects()
|
||||
.executeAndTransform(this::getArtifactResponse);
|
||||
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
|
||||
ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
|
||||
assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(artifactResponse.getSignature(), notNullValue());
|
||||
assertThat(artifactResponse.getAny(), instanceOf(ResponseType.class));
|
||||
assertThat(artifactResponse.getInResponseTo(), not(isEmptyOrNullString()));
|
||||
ResponseType samlResponse = (ResponseType)artifactResponse.getAny();
|
||||
assertThat(samlResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
|
||||
SamlDeployment deployment = SamlUtils.getSamlDeploymentForClient("sales-post");
|
||||
SamlProtocolUtils.verifyDocumentSignature(response.getSamlDocument(), deployment.getIDP().getSignatureValidationKeyLocator());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingLoginFullExchangeWithRedirect() {
|
||||
SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.REDIRECT)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
|
||||
.build()
|
||||
.login().user(bburkeUser).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).verifyRedirect(true).build()
|
||||
.doNotFollowRedirects()
|
||||
.executeAndTransform(this::getArtifactResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
|
||||
ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
|
||||
assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(artifactResponse.getAny(), instanceOf(ResponseType.class));
|
||||
assertThat(artifactResponse.getInResponseTo(), not(isEmptyOrNullString()));
|
||||
ResponseType samlResponse = (ResponseType)artifactResponse.getAny();
|
||||
assertThat(samlResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactResponseContainsCorrectInResponseTo(){
|
||||
SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.POST)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
|
||||
.build()
|
||||
.login().user(bburkeUser).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).setArtifactResolveId("TestId").build()
|
||||
.doNotFollowRedirects()
|
||||
.executeAndTransform(this::getArtifactResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
|
||||
ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
|
||||
assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(artifactResponse.getSignature(), nullValue());
|
||||
assertThat(artifactResponse.getAny(), instanceOf(ResponseType.class));
|
||||
assertThat(artifactResponse.getInResponseTo(), is("TestId"));
|
||||
}
|
||||
|
||||
/************************ LOGOUT TESTS ************************/
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingLogoutSingleClientCheckArtifact() throws NoSuchAlgorithmException {
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
|
||||
.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "true")
|
||||
.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url")
|
||||
.setFrontchannelLogout(true)
|
||||
.update()
|
||||
);
|
||||
|
||||
String response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.POST)
|
||||
.build()
|
||||
.login().user(bburkeUser).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).build()
|
||||
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SamlClient.Binding.POST).build()
|
||||
.doNotFollowRedirects()
|
||||
.executeAndTransform(resp -> EntityUtils.toString(resp.getEntity()));
|
||||
|
||||
assertThat(response, containsString(GeneralConstants.SAML_ARTIFACT_KEY));
|
||||
Pattern artifactPattern = Pattern.compile("NAME=\"SAMLart\" VALUE=\"((?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=))");
|
||||
Matcher m = artifactPattern.matcher(response);
|
||||
assertThat(true, is(m.find()));
|
||||
|
||||
String artifactB64 = m.group(1);
|
||||
assertThat(artifactB64, not(isEmptyOrNullString()));
|
||||
|
||||
byte[] artifact = Base64.getDecoder().decode(artifactB64);
|
||||
assertThat(artifact.length, is(44));
|
||||
assertThat(artifact[0], is((byte)0));
|
||||
assertThat(artifact[1], is((byte)4));
|
||||
assertThat(artifact[2], is((byte)0));
|
||||
assertThat(artifact[3], is((byte)0));
|
||||
|
||||
MessageDigest sha1Digester = MessageDigest.getInstance("SHA-1");
|
||||
byte[] source = sha1Digester.digest(getAuthServerRealmBase(REALM_NAME).toString().getBytes(Charsets.UTF_8));
|
||||
for (int i = 0; i < 20; i++) {
|
||||
assertThat(source[i], is(artifact[i+4]));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingLogoutSingleClientPost() {
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
|
||||
.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url")
|
||||
.setFrontchannelLogout(true)
|
||||
.update()
|
||||
);
|
||||
|
||||
SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST)
|
||||
.build()
|
||||
.login().user(bburkeUser).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).build()
|
||||
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).build()
|
||||
.doNotFollowRedirects()
|
||||
.executeAndTransform(this::getArtifactResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
|
||||
ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
|
||||
assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(artifactResponse.getSignature(), nullValue());
|
||||
assertThat(artifactResponse.getAny(), not(instanceOf(ResponseType.class)));
|
||||
assertThat(artifactResponse.getAny(), not(instanceOf(ArtifactResponseType.class)));
|
||||
assertThat(artifactResponse.getAny(), not(instanceOf(NameIDMappingResponseType.class)));
|
||||
assertThat(artifactResponse.getAny(), instanceOf(StatusResponseType.class));
|
||||
StatusResponseType samlResponse = (StatusResponseType)artifactResponse.getAny();
|
||||
assertThat(samlResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingLogoutSingleClientRedirect() {
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
|
||||
.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url")
|
||||
.setFrontchannelLogout(true)
|
||||
.update()
|
||||
);
|
||||
|
||||
SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, REDIRECT)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri())
|
||||
.build()
|
||||
.login().user(bburkeUser).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).verifyRedirect(true).build()
|
||||
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, REDIRECT).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).verifyRedirect(true).build()
|
||||
.doNotFollowRedirects()
|
||||
.executeAndTransform(this::getArtifactResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
|
||||
ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
|
||||
assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(artifactResponse.getSignature(), nullValue());
|
||||
assertThat(artifactResponse.getAny(), not(instanceOf(ResponseType.class)));
|
||||
assertThat(artifactResponse.getAny(), not(instanceOf(ArtifactResponseType.class)));
|
||||
assertThat(artifactResponse.getAny(), not(instanceOf(NameIDMappingResponseType.class)));
|
||||
assertThat(artifactResponse.getAny(), instanceOf(StatusResponseType.class));
|
||||
StatusResponseType samlResponse = (StatusResponseType)artifactResponse.getAny();
|
||||
assertThat(samlResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingLogoutTwoClientsPostWithSig() throws Exception {
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST_SIG)
|
||||
.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
|
||||
.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url")
|
||||
.setFrontchannelLogout(true)
|
||||
.update()
|
||||
);
|
||||
|
||||
SAMLDocumentHolder response = new SamlClientBuilder()
|
||||
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, SAML_ASSERTION_CONSUMER_URL_SALES_POST2, POST).build()
|
||||
.login().user(bburkeUser).build()
|
||||
.processSamlResponse(POST)
|
||||
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
||||
.build()
|
||||
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_SIG, SAML_ASSERTION_CONSUMER_URL_SALES_POST_SIG, POST)
|
||||
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||
.build()
|
||||
.login().sso(true).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_SIG)
|
||||
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||
.build()
|
||||
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, POST)
|
||||
.nameId(nameIdRef::get)
|
||||
.sessionIndex(sessionIndexRef::get)
|
||||
.build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_SIG)
|
||||
.signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
|
||||
.build()
|
||||
.doNotFollowRedirects()
|
||||
.executeAndTransform(this::getArtifactResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
|
||||
ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
|
||||
assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(artifactResponse.getSignature(), notNullValue());
|
||||
assertThat(artifactResponse.getAny(), instanceOf(LogoutRequestType.class));
|
||||
|
||||
SamlDeployment deployment = SamlUtils.getSamlDeploymentForClient("sales-post");
|
||||
SamlProtocolUtils.verifyDocumentSignature(response.getSamlDocument(), deployment.getIDP().getSignatureValidationKeyLocator());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingLogoutTwoClientsRedirect() {
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
|
||||
.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url")
|
||||
.setFrontchannelLogout(true)
|
||||
.update()
|
||||
)
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST2)
|
||||
.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url")
|
||||
.setFrontchannelLogout(true)
|
||||
.update()
|
||||
);
|
||||
|
||||
SAMLDocumentHolder response = new SamlClientBuilder()
|
||||
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, SAML_ASSERTION_CONSUMER_URL_SALES_POST2, REDIRECT)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri()).build()
|
||||
.login().user(bburkeUser).build()
|
||||
.processSamlResponse(REDIRECT)
|
||||
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
||||
.build()
|
||||
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, REDIRECT).setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri())
|
||||
.build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).verifyRedirect(true).build() // This is a formal step
|
||||
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, REDIRECT)
|
||||
.nameId(nameIdRef::get)
|
||||
.sessionIndex(sessionIndexRef::get)
|
||||
.build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).verifyRedirect(true).build()
|
||||
.doNotFollowRedirects()
|
||||
.executeAndTransform(this::getArtifactResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
|
||||
ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
|
||||
assertThat(artifactResponse.getSignature(), nullValue());
|
||||
assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(artifactResponse.getAny(), instanceOf(LogoutRequestType.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingWithBackchannelLogout() {
|
||||
try (SamlMessageReceiver backchannelLogoutReceiver = new SamlMessageReceiver(8082);
|
||||
ClientAttributeUpdater cau = ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
|
||||
.setFrontchannelLogout(false)
|
||||
.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, backchannelLogoutReceiver.getUrl())
|
||||
.update()) {
|
||||
|
||||
new SamlClientBuilder()
|
||||
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri()).build()
|
||||
.login().user(bburkeUser).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).build()
|
||||
|
||||
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, SAML_ASSERTION_CONSUMER_URL_SALES_POST2, POST)
|
||||
.build()
|
||||
.followOneRedirect()
|
||||
.processSamlResponse(POST)
|
||||
.transformObject(this::extractNameIdAndSessionIndexAndTerminate)
|
||||
.build()
|
||||
.execute();
|
||||
|
||||
// We need new SamlClient so that logout is not done using cookie -> frontchannel logout
|
||||
new SamlClientBuilder()
|
||||
// Initiate logout as SAML_CLIENT_ID_SALES_POST2 and expect backchannel call to SAML_CLIENT_ID_SALES_POST
|
||||
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, POST)
|
||||
.nameId(nameIdRef::get)
|
||||
.sessionIndex(sessionIndexRef::get)
|
||||
.build()
|
||||
.executeAndTransform(r -> {
|
||||
SAMLDocumentHolder saml2ObjectHolder = POST.extractResponse(r);
|
||||
assertThat(saml2ObjectHolder.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Check whether logoutReceiver contains correct LogoutRequest
|
||||
await()
|
||||
.pollInterval(100, TimeUnit.MILLISECONDS)
|
||||
.atMost(1, TimeUnit.MINUTES)
|
||||
.until(backchannelLogoutReceiver::isMessageReceived);
|
||||
|
||||
assertThat(backchannelLogoutReceiver.isMessageReceived(), is(true));
|
||||
SAMLDocumentHolder message = backchannelLogoutReceiver.getSamlDocumentHolder();
|
||||
|
||||
assertThat(message.getSamlObject(), isSamlLogoutRequest(backchannelLogoutReceiver.getUrl()));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Cannot run SamlMessageReceiver", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactResolveWithWrongIssuerFails() {
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
|
||||
.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "http://url")
|
||||
.update()
|
||||
);
|
||||
|
||||
new SamlClientBuilder()
|
||||
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri()).build()
|
||||
.login().user(bburkeUser).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2).build() // Wrong issuer
|
||||
.execute(r -> assertThat(r, bodyHC(containsString(JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get()))));
|
||||
}
|
||||
|
||||
@AuthServerContainerExclude(AuthServerContainerExclude.AuthServer.REMOTE) // Won't work with openshift, because openshift wouldn't see ArtifactResolutionService
|
||||
@Test
|
||||
public void testSessionStateDuringArtifactBindingLogoutWithOneClient() {
|
||||
ClientRepresentation salesRep = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST).get(0);
|
||||
final String clientId = salesRep.getId();
|
||||
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
|
||||
.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url")
|
||||
.setFrontchannelLogout(true)
|
||||
.update()
|
||||
);
|
||||
|
||||
AtomicReference<String> userSessionId = new AtomicReference<>();
|
||||
|
||||
SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST)
|
||||
.build()
|
||||
.login().user(bburkeUser)
|
||||
.build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST)
|
||||
.setBeforeStepChecks(new SessionStateChecker(testingClient.server())
|
||||
.storeUserSessionId(userSessionId)
|
||||
.expectedState(UserSessionModel.State.LOGGED_IN)
|
||||
.expectedClientSession(clientId)
|
||||
.consumeUserSession(userSessionModel -> assertThat(userSessionModel, notNullValue()))
|
||||
.consumeClientSession(clientId, userSessionModel -> assertThat(userSessionModel, notNullValue())))
|
||||
.build()
|
||||
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST)
|
||||
.setBeforeStepChecks(new SessionStateChecker(testingClient.server())
|
||||
.expectedUserSession(userSessionId)
|
||||
.expectedState(UserSessionModel.State.LOGGED_OUT_UNCONFIRMED)
|
||||
.expectedNumberOfClientSessions(1)
|
||||
.expectedAction(clientId, CommonClientSessionModel.Action.LOGGING_OUT))
|
||||
.setAfterStepChecks(new SessionStateChecker(testingClient.server())
|
||||
.consumeUserSession(userSessionModel -> assertThat(userSessionModel, nullValue()))
|
||||
.setUserSessionProvider(session -> userSessionId.get()))
|
||||
.build()
|
||||
.doNotFollowRedirects().executeAndTransform(this::getArtifactResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
|
||||
ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
|
||||
assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(artifactResponse.getSignature(), nullValue());
|
||||
assertThat(artifactResponse.getAny(), not(instanceOf(ResponseType.class)));
|
||||
assertThat(artifactResponse.getAny(), not(instanceOf(ArtifactResponseType.class)));
|
||||
assertThat(artifactResponse.getAny(), not(instanceOf(NameIDMappingResponseType.class)));
|
||||
assertThat(artifactResponse.getAny(), instanceOf(StatusResponseType.class));
|
||||
StatusResponseType samlResponse = (StatusResponseType)artifactResponse.getAny();
|
||||
assertThat(samlResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
}
|
||||
|
||||
@AuthServerContainerExclude(AuthServerContainerExclude.AuthServer.REMOTE) // Won't work with openshift, because openshift wouldn't see ArtifactResolutionService
|
||||
@Test
|
||||
public void testSessionStateDuringArtifactBindingLogoutWithMoreFrontChannelClients() {
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
|
||||
.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url")
|
||||
.setFrontchannelLogout(true)
|
||||
.update()
|
||||
)
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST2)
|
||||
.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
|
||||
.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url")
|
||||
.setFrontchannelLogout(true)
|
||||
.update()
|
||||
);
|
||||
|
||||
ClientRepresentation salesRep = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST).get(0);
|
||||
final String salesRepId = salesRep.getId();
|
||||
|
||||
ClientRepresentation salesRep2 = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST2).get(0);
|
||||
final String salesRep2Id = salesRep2.getId();
|
||||
|
||||
final AtomicReference<String> userSessionId = new AtomicReference<>();
|
||||
|
||||
SAMLDocumentHolder response = new SamlClientBuilder()
|
||||
// Login first sales_post2 and resolve artifact
|
||||
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, SAML_ASSERTION_CONSUMER_URL_SALES_POST2, REDIRECT)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri()).build()
|
||||
.login().user(bburkeUser).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2)
|
||||
.setBeforeStepChecks(new SessionStateChecker(testingClient.server())
|
||||
.storeUserSessionId(userSessionId)
|
||||
.expectedClientSession(salesRep2Id)
|
||||
.expectedState(UserSessionModel.State.LOGGED_IN)
|
||||
.expectedNumberOfClientSessions(1)
|
||||
.consumeUserSession(userSessionModel -> assertThat(userSessionModel, notNullValue()))
|
||||
.consumeClientSession(salesRep2Id, clientSession -> assertThat(clientSession, notNullValue())))
|
||||
.verifyRedirect(true)
|
||||
.build() // This is a formal step
|
||||
|
||||
// Login second sales_post and resolved artifact, no login should be needed as user is already logged in
|
||||
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, REDIRECT)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri())
|
||||
.build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST)
|
||||
.setBeforeStepChecks(new SessionStateChecker(testingClient.server())
|
||||
.expectedUserSession(userSessionId)
|
||||
.expectedState(UserSessionModel.State.LOGGED_IN)
|
||||
.expectedClientSession(salesRepId)
|
||||
.expectedNumberOfClientSessions(2)
|
||||
.expectedAction(salesRep2Id, null)
|
||||
.expectedAction(salesRepId, null))
|
||||
.verifyRedirect(true)
|
||||
.build()
|
||||
|
||||
// Initiate logout from sales_post2
|
||||
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, REDIRECT)
|
||||
.build()
|
||||
|
||||
// Since sales_post uses frontchannel logout, keycloak should send LogoutRequest to sales_post first
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST)
|
||||
.setBeforeStepChecks(new SessionStateChecker(testingClient.server())
|
||||
.expectedUserSession(userSessionId)
|
||||
.expectedState(UserSessionModel.State.LOGGING_OUT)
|
||||
.expectedClientSession(salesRepId)
|
||||
.expectedNumberOfClientSessions(2)
|
||||
.expectedAction(salesRepId, CommonClientSessionModel.Action.LOGGING_OUT)
|
||||
.expectedAction(salesRep2Id, CommonClientSessionModel.Action.LOGGING_OUT))
|
||||
.setAfterStepChecks(new SessionStateChecker(testingClient.server())
|
||||
.setUserSessionProvider(session -> userSessionId.get())
|
||||
.expectedState(UserSessionModel.State.LOGGING_OUT)
|
||||
.expectedNumberOfClientSessions(2)
|
||||
.expectedAction(salesRepId, CommonClientSessionModel.Action.LOGGED_OUT)
|
||||
.expectedAction(salesRep2Id, CommonClientSessionModel.Action.LOGGING_OUT))
|
||||
.verifyRedirect(true)
|
||||
.build()
|
||||
.doNotFollowRedirects()
|
||||
|
||||
// Respond with LogoutResponse so that logout flow can continue with logging out client2
|
||||
.processSamlResponse(ARTIFACT_RESPONSE)
|
||||
.transformDocument(doc -> {
|
||||
// Send LogoutResponse
|
||||
SAML2Object so = (SAML2Object) SAMLParser.getInstance().parse(new DOMSource(doc));
|
||||
return new SAML2LogoutResponseBuilder()
|
||||
.destination(getAuthServerSamlEndpoint(REALM_NAME).toString())
|
||||
.issuer(SAML_CLIENT_ID_SALES_POST)
|
||||
.logoutRequestID(((LogoutRequestType) so).getID())
|
||||
.buildDocument();
|
||||
})
|
||||
.targetBinding(REDIRECT)
|
||||
.targetAttributeSamlResponse()
|
||||
.targetUri(getAuthServerSamlEndpoint(REALM_NAME))
|
||||
.build()
|
||||
|
||||
// Now Keycloak should finish logout process so it should respond with LogoutResponse
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2)
|
||||
.verifyRedirect(true)
|
||||
.setBeforeStepChecks(new SessionStateChecker(testingClient.server())
|
||||
.expectedUserSession(userSessionId)
|
||||
.expectedClientSession(salesRep2Id)
|
||||
.expectedState(UserSessionModel.State.LOGGED_OUT_UNCONFIRMED)
|
||||
.expectedNumberOfClientSessions(2)
|
||||
.expectedAction(salesRepId, CommonClientSessionModel.Action.LOGGED_OUT)
|
||||
.expectedAction(salesRep2Id, CommonClientSessionModel.Action.LOGGING_OUT))
|
||||
.setAfterStepChecks(new SessionStateChecker(testingClient.server())
|
||||
.consumeUserSession(userSessionModel -> assertThat(userSessionModel, nullValue()))
|
||||
.setUserSessionProvider(session -> userSessionId.get()))
|
||||
.build()
|
||||
.executeAndTransform(this::getArtifactResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
|
||||
ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
|
||||
assertThat(artifactResponse.getSignature(), nullValue());
|
||||
assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(artifactResponse.getAny(), instanceOf(StatusResponseType.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingIsNotUsedForLogoutWhenLogoutUrlNotSetRedirect() {
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
|
||||
.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "http://url")
|
||||
.setFrontchannelLogout(true)
|
||||
.update()
|
||||
);
|
||||
|
||||
SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, REDIRECT)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri())
|
||||
.build()
|
||||
.login().user(bburkeUser).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).verifyRedirect(true).build()
|
||||
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, REDIRECT).build()
|
||||
.doNotFollowRedirects()
|
||||
.executeAndTransform(REDIRECT::extractResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
|
||||
StatusResponseType logoutResponse = (StatusResponseType)response.getSamlObject();
|
||||
assertThat(logoutResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(logoutResponse.getSignature(), nullValue());
|
||||
assertThat(logoutResponse, not(instanceOf(ResponseType.class)));
|
||||
assertThat(logoutResponse, not(instanceOf(ArtifactResponseType.class)));
|
||||
assertThat(logoutResponse, not(instanceOf(NameIDMappingResponseType.class)));
|
||||
assertThat(logoutResponse, instanceOf(StatusResponseType.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArtifactBindingIsNotUsedForLogoutWhenLogoutUrlNotSetPostTest() {
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
|
||||
.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "http://url")
|
||||
.setFrontchannelLogout(true)
|
||||
.update()
|
||||
);
|
||||
|
||||
SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.getUri())
|
||||
.build()
|
||||
.login().user(bburkeUser).build()
|
||||
.handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).build()
|
||||
.logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST).build()
|
||||
.doNotFollowRedirects()
|
||||
.executeAndTransform(POST::extractResponse);
|
||||
|
||||
assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
|
||||
StatusResponseType logoutResponse = (StatusResponseType)response.getSamlObject();
|
||||
assertThat(logoutResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(logoutResponse.getSignature(), nullValue());
|
||||
assertThat(logoutResponse, not(instanceOf(ResponseType.class)));
|
||||
assertThat(logoutResponse, not(instanceOf(ArtifactResponseType.class)));
|
||||
assertThat(logoutResponse, not(instanceOf(NameIDMappingResponseType.class)));
|
||||
assertThat(logoutResponse, instanceOf(StatusResponseType.class));
|
||||
}
|
||||
|
||||
private SAMLDocumentHolder getArtifactResponse(CloseableHttpResponse response) throws IOException, ParsingException, ProcessingException {
|
||||
assertThat(response, statusCodeIsHC(Response.Status.OK));
|
||||
Document soapBody = extractSoapMessage(response);
|
||||
return SAML2Request.getSAML2ObjectFromDocument(soapBody);
|
||||
}
|
||||
|
||||
private Document extractSoapMessage(CloseableHttpResponse response) throws IOException {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(EntityUtils.toByteArray(response.getEntity()));
|
||||
Document soapBody = Soap.extractSoapMessage(bais);
|
||||
response.close();
|
||||
return soapBody;
|
||||
}
|
||||
|
||||
/************************ IMPORT CLIENT TESTS ************************/
|
||||
@Test
|
||||
public void testImportClientArtifactResolutionSingleServices() {
|
||||
Document doc = IOUtil.loadXML(ArtifactBindingTest.class.getResourceAsStream("/saml/sp-metadata-artifact-simple.xml"));
|
||||
ClientRepresentation clientRep = adminClient.realm(REALM_NAME).convertClientDescription(IOUtil.documentToString(doc));
|
||||
assertThat(clientRep.getAttributes().get(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE), is("https://test.keycloak.com/auth/login/epd/callback/soap"));
|
||||
assertThat(clientRep.getAttributes().get(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE), is("https://test.keycloak.com/auth/login/epd/callback/http-artifact"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImportClientMultipleServices() {
|
||||
Document doc = IOUtil.loadXML(ArtifactBindingTest.class.getResourceAsStream("/saml/sp-metadata-artifact-multiple.xml"));
|
||||
ClientRepresentation clientRep = adminClient.realm(REALM_NAME).convertClientDescription(IOUtil.documentToString(doc));
|
||||
assertThat(clientRep.getAttributes().get(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE), is("https://test.keycloak.com/auth/login/epd/callback/soap-1"));
|
||||
assertThat(clientRep.getAttributes().get(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE), Matchers.startsWith("https://test.keycloak.com/auth/login/epd/callback/http-artifact"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImportClientMultipleServicesWithDefault() {
|
||||
Document doc = IOUtil.loadXML(ArtifactBindingTest.class.getResourceAsStream("/saml/sp-metadata-artifact-multiple-default.xml"));
|
||||
ClientRepresentation clientRep = adminClient.realm(REALM_NAME).convertClientDescription(IOUtil.documentToString(doc));
|
||||
assertThat(clientRep.getAttributes().get(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE), is("https://test.keycloak.com/auth/login/epd/callback/soap-9"));
|
||||
assertThat(clientRep.getAttributes().get(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE), Matchers.startsWith("https://test.keycloak.com/auth/login/epd/callback/http-artifact"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSPMetadataArtifactBindingNotUsedForLogout() throws ParsingException, URISyntaxException {
|
||||
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
|
||||
.setAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE, "http://url.artifact.test")
|
||||
.setAdminUrl("http://admin.url.test")
|
||||
.update()
|
||||
);
|
||||
SPSSODescriptorType spDescriptor = getSPInstallationDescriptor(adminClient.realm(REALM_NAME).clients(), SAML_CLIENT_ID_SALES_POST);
|
||||
assertThat(spDescriptor.getAssertionConsumerService().get(0).getBinding(), is(equalTo(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())));
|
||||
assertThat(spDescriptor.getAssertionConsumerService().get(0).getLocation(), is(equalTo(new URI("http://url.artifact.test"))));
|
||||
|
||||
assertThat(spDescriptor.getSingleLogoutService().get(0).getBinding(), is(equalTo(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri())));
|
||||
assertThat(spDescriptor.getSingleLogoutService().get(0).getLocation(), is(equalTo(new URI("http://admin.url.test"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSPMetadataArtifactBindingUsedForLogout() throws ParsingException, URISyntaxException {
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
|
||||
.setAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE, "http://url.artifact.test")
|
||||
.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url.artifact.test")
|
||||
.setAdminUrl("http://admin.url.test")
|
||||
.update()
|
||||
);
|
||||
|
||||
SPSSODescriptorType spDescriptor = getSPInstallationDescriptor(adminClient.realm(REALM_NAME).clients(), SAML_CLIENT_ID_SALES_POST);
|
||||
assertThat(spDescriptor.getAssertionConsumerService().get(0).getBinding(), is(equalTo(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())));
|
||||
assertThat(spDescriptor.getAssertionConsumerService().get(0).getLocation(), is(equalTo(new URI("http://url.artifact.test"))));
|
||||
|
||||
assertThat(spDescriptor.getSingleLogoutService().get(0).getBinding(), is(equalTo(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())));
|
||||
assertThat(spDescriptor.getSingleLogoutService().get(0).getLocation(), is(equalTo(new URI("http://url.artifact.test"))));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,265 @@
|
|||
package org.keycloak.testsuite.saml;
|
||||
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
|
||||
import org.keycloak.protocol.saml.SamlProtocol;
|
||||
import org.keycloak.saml.SAML2LogoutRequestBuilder;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
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 org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
|
||||
import org.keycloak.testsuite.util.ArtifactResolutionService;
|
||||
import org.keycloak.testsuite.util.SamlClient;
|
||||
import org.keycloak.testsuite.util.SamlClientBuilder;
|
||||
import org.keycloak.testsuite.util.saml.CreateArtifactMessageStepBuilder;
|
||||
import org.w3c.dom.Document;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.keycloak.testsuite.util.Matchers.bodyHC;
|
||||
import static org.keycloak.testsuite.util.Matchers.isSamlStatusResponse;
|
||||
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
|
||||
import static org.keycloak.testsuite.util.SamlClient.Binding.POST;
|
||||
import static org.keycloak.testsuite.util.SamlClient.Binding.REDIRECT;
|
||||
|
||||
@AuthServerContainerExclude(AuthServerContainerExclude.AuthServer.REMOTE) // Won't work with openshift, because openshift wouldn't see ArtifactResolutionService
|
||||
public class ArtifactBindingWithResolutionServiceTest extends AbstractSamlTest {
|
||||
|
||||
@Test
|
||||
public void testReceiveArtifactLoginFullWithPost() throws ParsingException, ConfigurationException, ProcessingException, InterruptedException {
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE, "http://127.0.0.1:8082/")
|
||||
.update()
|
||||
);
|
||||
|
||||
AuthnRequestType loginRep = SamlClient.createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, AbstractSamlTest.SAML_ASSERTION_CONSUMER_URL_SALES_POST, null);
|
||||
Document doc = SAML2Request.convert(loginRep);
|
||||
|
||||
SamlClientBuilder builder = new SamlClientBuilder();
|
||||
CreateArtifactMessageStepBuilder camb = new CreateArtifactMessageStepBuilder(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SamlClient.Binding.POST, builder);
|
||||
|
||||
ArtifactResolutionService ars = new ArtifactResolutionService("http://127.0.0.1:8082/").setResponseDocument(doc);
|
||||
Thread arsThread = new Thread(ars);
|
||||
try {
|
||||
arsThread.start();
|
||||
synchronized (ars) {
|
||||
ars.wait();
|
||||
SAMLDocumentHolder response = builder.artifactMessage(camb).build().login().user(bburkeUser).build().getSamlResponse(SamlClient.Binding.POST);
|
||||
assertThat(response.getSamlObject(), instanceOf(ResponseType.class));
|
||||
ResponseType rt = (ResponseType)response.getSamlObject();
|
||||
assertThat(rt.getAssertions(),not(empty()));
|
||||
assertThat(ars.getLastArtifactResolve(), notNullValue());
|
||||
assertThat(camb.getLastArtifact(), is(ars.getLastArtifactResolve().getArtifact()));
|
||||
}
|
||||
} finally {
|
||||
ars.stop();
|
||||
arsThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveArtifactLoginFullWithRedirect() throws ParsingException, ConfigurationException, ProcessingException, InterruptedException {
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE, "http://127.0.0.1:8082/")
|
||||
.update()
|
||||
);
|
||||
|
||||
AuthnRequestType loginReq = SamlClient.createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, AbstractSamlTest.SAML_ASSERTION_CONSUMER_URL_SALES_POST, null);
|
||||
loginReq.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri());
|
||||
Document doc = SAML2Request.convert(loginReq);
|
||||
|
||||
SamlClientBuilder builder = new SamlClientBuilder();
|
||||
CreateArtifactMessageStepBuilder camb = new CreateArtifactMessageStepBuilder(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SamlClient.Binding.REDIRECT, builder);
|
||||
|
||||
ArtifactResolutionService ars = new ArtifactResolutionService("http://127.0.0.1:8082/").setResponseDocument(doc);
|
||||
Thread arsThread = new Thread(ars);
|
||||
try {
|
||||
arsThread.start();
|
||||
synchronized (ars) {
|
||||
ars.wait();
|
||||
SAMLDocumentHolder response = builder.artifactMessage(camb).build().login().user(bburkeUser).build().getSamlResponse(REDIRECT);
|
||||
assertThat(response.getSamlObject(), instanceOf(ResponseType.class));
|
||||
ResponseType rt = (ResponseType)response.getSamlObject();
|
||||
assertThat(rt.getAssertions(),not(empty()));
|
||||
assertThat(ars.getLastArtifactResolve(), notNullValue());
|
||||
assertThat(camb.getLastArtifact(), is(ars.getLastArtifactResolve().getArtifact()));
|
||||
}
|
||||
} finally {
|
||||
ars.stop();
|
||||
arsThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveArtifactNonExistingClient() throws ParsingException, ConfigurationException, ProcessingException, InterruptedException {
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE, "http://127.0.0.1:8082/")
|
||||
.update()
|
||||
);
|
||||
|
||||
AuthnRequestType loginRep = SamlClient.createLoginRequestDocument("blabla", AbstractSamlTest.SAML_ASSERTION_CONSUMER_URL_SALES_POST, null);
|
||||
Document doc = SAML2Request.convert(loginRep);
|
||||
|
||||
SamlClientBuilder builder = new SamlClientBuilder();
|
||||
CreateArtifactMessageStepBuilder camb = new CreateArtifactMessageStepBuilder(getAuthServerSamlEndpoint(REALM_NAME), "blabla",
|
||||
SamlClient.Binding.POST, builder);
|
||||
|
||||
ArtifactResolutionService ars = new ArtifactResolutionService("http://127.0.0.1:8082/").setResponseDocument(doc);
|
||||
Thread arsThread = new Thread(ars);
|
||||
try {
|
||||
arsThread.start();
|
||||
synchronized (ars) {
|
||||
ars.wait();
|
||||
String response = builder.artifactMessage(camb).build().executeAndTransform(resp -> EntityUtils.toString(resp.getEntity()));
|
||||
assertThat(response, containsString("Invalid Request"));
|
||||
}
|
||||
} finally {
|
||||
ars.stop();
|
||||
arsThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveEmptyArtifactResponse() throws InterruptedException {
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE, "http://127.0.0.1:8082/")
|
||||
.update()
|
||||
);
|
||||
|
||||
SamlClientBuilder builder = new SamlClientBuilder();
|
||||
CreateArtifactMessageStepBuilder camb = new CreateArtifactMessageStepBuilder(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SamlClient.Binding.POST, builder);
|
||||
|
||||
ArtifactResolutionService ars = new ArtifactResolutionService("http://127.0.0.1:8082/").setEmptyArtifactResponse(SAML_CLIENT_ID_SALES_POST);
|
||||
Thread arsThread = new Thread(ars);
|
||||
try {
|
||||
arsThread.start();
|
||||
synchronized (ars) {
|
||||
ars.wait();
|
||||
builder.artifactMessage(camb).build().execute(r -> {
|
||||
assertThat(r, statusCodeIsHC(400));
|
||||
assertThat(r, bodyHC(containsString("Unable to resolve artifact.")));
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
ars.stop();
|
||||
arsThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveArtifactLogoutFullWithPost() throws InterruptedException {
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE, "http://127.0.0.1:8082/")
|
||||
.update()
|
||||
);
|
||||
|
||||
SamlClientBuilder builder = new SamlClientBuilder();
|
||||
CreateArtifactMessageStepBuilder camb = new CreateArtifactMessageStepBuilder(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
POST, builder);
|
||||
|
||||
ArtifactResolutionService ars = new ArtifactResolutionService("http://127.0.0.1:8082/");
|
||||
Thread arsThread = new Thread(ars);
|
||||
try {
|
||||
arsThread.start();
|
||||
synchronized (ars) {
|
||||
ars.wait();
|
||||
SAMLDocumentHolder samlResponse = builder.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST).build()
|
||||
.login().user(bburkeUser).build()
|
||||
.processSamlResponse(POST)
|
||||
.transformObject(x -> {
|
||||
SAML2Object samlObj = extractNameIdAndSessionIndexAndTerminate(x);
|
||||
setArtifactResolutionServiceLogoutRequest(ars);
|
||||
return samlObj;
|
||||
})
|
||||
.build().artifactMessage(camb).build().getSamlResponse(POST);
|
||||
assertThat(samlResponse.getSamlObject(), instanceOf(StatusResponseType.class));
|
||||
StatusResponseType srt = (StatusResponseType) samlResponse.getSamlObject();
|
||||
assertThat(srt, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(camb.getLastArtifact(), is(ars.getLastArtifactResolve().getArtifact()));
|
||||
}
|
||||
} finally {
|
||||
ars.stop();
|
||||
arsThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReceiveArtifactLogoutFullWithRedirect() throws InterruptedException {
|
||||
getCleanup()
|
||||
.addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
|
||||
.setAttribute(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE, "http://127.0.0.1:8082/")
|
||||
.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "http://url")
|
||||
.setFrontchannelLogout(true)
|
||||
.update()
|
||||
);
|
||||
|
||||
SamlClientBuilder builder = new SamlClientBuilder();
|
||||
CreateArtifactMessageStepBuilder camb = new CreateArtifactMessageStepBuilder(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
REDIRECT, builder);
|
||||
|
||||
ArtifactResolutionService ars = new ArtifactResolutionService("http://127.0.0.1:8082/");
|
||||
Thread arsThread = new Thread(ars);
|
||||
try {
|
||||
arsThread.start();
|
||||
synchronized (ars) {
|
||||
ars.wait();
|
||||
SAMLDocumentHolder samlResponse = builder
|
||||
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
|
||||
SAML_ASSERTION_CONSUMER_URL_SALES_POST, REDIRECT)
|
||||
.setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri())
|
||||
.build()
|
||||
.login().user(bburkeUser).build()
|
||||
.processSamlResponse(REDIRECT)
|
||||
.transformObject(x -> {
|
||||
SAML2Object samlObj = extractNameIdAndSessionIndexAndTerminate(x);
|
||||
setArtifactResolutionServiceLogoutRequest(ars);
|
||||
return samlObj;
|
||||
})
|
||||
.build().artifactMessage(camb).build().getSamlResponse(REDIRECT);
|
||||
assertThat(samlResponse.getSamlObject(), instanceOf(StatusResponseType.class));
|
||||
StatusResponseType srt = (StatusResponseType) samlResponse.getSamlObject();
|
||||
assertThat(srt, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
assertThat(camb.getLastArtifact(), is(ars.getLastArtifactResolve().getArtifact()));
|
||||
}
|
||||
} finally {
|
||||
ars.stop();
|
||||
arsThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
private void setArtifactResolutionServiceLogoutRequest(ArtifactResolutionService ars) throws ParsingException, ConfigurationException, ProcessingException {
|
||||
SAML2LogoutRequestBuilder builder = new SAML2LogoutRequestBuilder()
|
||||
.destination(getAuthServerSamlEndpoint(REALM_NAME).toString())
|
||||
.issuer(SAML_CLIENT_ID_SALES_POST)
|
||||
.sessionIndex(sessionIndexRef.get());
|
||||
|
||||
final NameIDType nameIdValue = nameIdRef.get();
|
||||
|
||||
if (nameIdValue != null) {
|
||||
builder = builder.userPrincipal(nameIdValue.getValue(), nameIdValue.getFormat() == null ? null : nameIdValue.getFormat().toString());
|
||||
}
|
||||
ars.setResponseDocument(builder.buildDocument());
|
||||
}
|
||||
}
|
|
@ -128,7 +128,7 @@
|
|||
|
||||
"connectionsHttpClient": {
|
||||
"default": {
|
||||
"max-connection-idle-time-millis": 100
|
||||
"reuse-connections": false
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -235,5 +235,9 @@
|
|||
"dir": "target/dependency/vault",
|
||||
"enabled": "${keycloak.vault.files-plaintext.provider.enabled:false}"
|
||||
}
|
||||
},
|
||||
|
||||
"saml-artifact-resolver": {
|
||||
"provider": "${keycloak.saml-artifact-resolver.provider:default}"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<md:EntityDescriptor entityID="https://keycloak.com" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
|
||||
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<md:Extensions>
|
||||
<init:RequestInitiator Binding="urn:oasis:names:tc:SAML:profiles:SSO:request-init" Location="https://test.keycloak.com/auth/login/epd" xmlns:init="urn:oasis:names:tc:SAML:profiles:SSO:request-init"/>
|
||||
</md:Extensions>
|
||||
<md:KeyDescriptor>
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:KeyName>keycloak</ds:KeyName>
|
||||
<ds:X509Data>
|
||||
<ds:X509SubjectName>CN=keycloak.com</ds:X509SubjectName>
|
||||
<ds:X509Certificate>MIIFDzCCAvegAwIBAgIUM+Ho+HIxh9p4yV8qT5S+SsyYlY4wDQYJKoZIhvcNAQELBQAwFzEVMBMGA1UEAwwMa2V5Y2xvYWsuY29tMB4XDTE5MDQwNTEzMjAzNFoXDTI5MDQwMjEzMjAzNFowFzEVMBMGA1UEAwwMa2V5Y2xvYWsuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAu+VA5xDggY+wfkA7LFcmRI45UPpUp47nMKVOF11f4jh7XfjztdOcKILDaYdg1N8Ldm1DUZTi81KkTQdUg8DjyaJj/di12UKVJAusaGxfaF6XfVihHtxckVGAuXU4BbPdQZ+qMiPNw0G/mFIIU+9ykIApjAyU4eHuKmjI83oXzCsN5bxZmzcR5QKDa/AwTQtpaTNd7vStm5mS+lIQIwB3g9vYYIIzasoP/H27MfeAg+7jK9BsKQrfUJ+GYsb5S3NBor6K4laeBslbKJUaBu29ekPqLacrwdaq1TJXpbfSOLYlM0vzJmlN/SVavqM3eBvFzIYD4VKg43JwQd/7W7cGVnoawmbaWMPYvZojeNeU9j94BKNchQX606ROqKuAM50zA6m8b8Y6KwF4zHexHDuZXBYTtk/HsDnrO7Y6Hz0KzEtqj/E5YHukvhSYkKj+DP/8nPnJOCE48tVRqmlhMs8LDR5DA1SI4Z+jAiFEuYa6tMFjUTTYl6O1ZijJCTe+K6p4OgfdEUAA3cwdvsGz6jYrJXB1v9WiZKQRLV6LVkPqH6TUcx/hV3Ca9J9+GkkynMDkKBNo6EZZ/XX5pielc2CtSE4vR78rIzkNMpl5DhOh6iDlrT2dI86soGtdSDXm7DQK0HOjZzFpaDl9Q0Xp3C7wgjl2i3JzULLEsOWyzIfE8+ECAwEAAaNTMFEwHQYDVR0OBBYEFO2ScoBwXTfoY24DnYubWuYVKONbMB8GA1UdIwQYMBaAFO2ScoBwXTfoY24DnYubWuYVKONbMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBADhbclqOYQwh5cDNq1pdA4XqBNPSGRg9liu84JzuhWnADAO+4fmyr9D1Usr2te1NW5pzGWxcOI8kg05l7ZPut0Vu937aBf9UAR5Q0qK2aV6450kmC4q7+Kw1qEsRbefs/90PFAI7mCnnVAuUCpdZNz8RwONAGsgS43tpfF4ZM5nXYBsFoLP3jQBWeepOx4EkJK+wjywJ55aLpJPznrYH1C1N6Du+Aon1YKkPSzpNEpt2+LBr0844EZOVhrBoL08ClGEgThk6uNycRL0I7nDPddY+27lCqDk5UimMdonvDaXC5K/5Lmf+EWzv3vp6X00e1Wo5YrYZSIv8LLhG9VU8+AfZ1NHmEvEaVz1Dx1H9yl6K9RZ9RRq+bsq6GwCUCqdBkCyFY2jkdpgpcADj1bXTTdKkKEGbq8z5yUKDoXfwmxRJMQmqYh1r61nHG8XLriO8FQc5qUJQpI+ITH8nfqtd4VTDs7QIopWfPihx/r5vk11jB5cGKwGzg5+IlcsQK0OW+se42h0/WNgcMz+rimJzuQta30xPR26McquC8IbrpJhgjwLFEfoZgxHVOGFC9/O8Gc+xi62prly81R1bbhnYCy/BUILEz56yWyXojATpjBacoZTW5EwIHoFjagngi5pOznmzbnbKMhLlioj8IHm/s8YYchBU3mKYufDg+B/jASoI</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://test.keycloak.com/auth/login/epd/callback/soap-5" index="5"/>
|
||||
<md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://test.keycloak.com/auth/login/epd/callback/soap-1" index="1"/>
|
||||
<md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://test.keycloak.com/auth/login/epd/callback/soap-9" index="9" isDefault="true"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://test.keycloak.com/auth/login/epd/callback/http-artifact-8" index="8"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://test.keycloak.com/auth/login/epd/callback/http-post" index="2"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://test.keycloak.com/auth/login/epd/callback/http-post-simplesign" index="3"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS" Location="https://test.keycloak.com/auth/login/epd/callback/paos" index="4"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://test.keycloak.com/auth/login/epd/callback/http-artifact-6" index="6"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://test.keycloak.com/auth/login/epd/callback/http-artifact-7" index="7"/>
|
||||
</md:SPSSODescriptor>
|
||||
</md:EntityDescriptor>
|
|
@ -0,0 +1,25 @@
|
|||
<md:EntityDescriptor entityID="https://keycloak.com" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
|
||||
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<md:Extensions>
|
||||
<init:RequestInitiator Binding="urn:oasis:names:tc:SAML:profiles:SSO:request-init" Location="https://test.keycloak.com/auth/login/epd" xmlns:init="urn:oasis:names:tc:SAML:profiles:SSO:request-init"/>
|
||||
</md:Extensions>
|
||||
<md:KeyDescriptor>
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:KeyName>keycloak</ds:KeyName>
|
||||
<ds:X509Data>
|
||||
<ds:X509SubjectName>CN=keycloak.com</ds:X509SubjectName>
|
||||
<ds:X509Certificate>MIIFDzCCAvegAwIBAgIUM+Ho+HIxh9p4yV8qT5S+SsyYlY4wDQYJKoZIhvcNAQELBQAwFzEVMBMGA1UEAwwMa2V5Y2xvYWsuY29tMB4XDTE5MDQwNTEzMjAzNFoXDTI5MDQwMjEzMjAzNFowFzEVMBMGA1UEAwwMa2V5Y2xvYWsuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAu+VA5xDggY+wfkA7LFcmRI45UPpUp47nMKVOF11f4jh7XfjztdOcKILDaYdg1N8Ldm1DUZTi81KkTQdUg8DjyaJj/di12UKVJAusaGxfaF6XfVihHtxckVGAuXU4BbPdQZ+qMiPNw0G/mFIIU+9ykIApjAyU4eHuKmjI83oXzCsN5bxZmzcR5QKDa/AwTQtpaTNd7vStm5mS+lIQIwB3g9vYYIIzasoP/H27MfeAg+7jK9BsKQrfUJ+GYsb5S3NBor6K4laeBslbKJUaBu29ekPqLacrwdaq1TJXpbfSOLYlM0vzJmlN/SVavqM3eBvFzIYD4VKg43JwQd/7W7cGVnoawmbaWMPYvZojeNeU9j94BKNchQX606ROqKuAM50zA6m8b8Y6KwF4zHexHDuZXBYTtk/HsDnrO7Y6Hz0KzEtqj/E5YHukvhSYkKj+DP/8nPnJOCE48tVRqmlhMs8LDR5DA1SI4Z+jAiFEuYa6tMFjUTTYl6O1ZijJCTe+K6p4OgfdEUAA3cwdvsGz6jYrJXB1v9WiZKQRLV6LVkPqH6TUcx/hV3Ca9J9+GkkynMDkKBNo6EZZ/XX5pielc2CtSE4vR78rIzkNMpl5DhOh6iDlrT2dI86soGtdSDXm7DQK0HOjZzFpaDl9Q0Xp3C7wgjl2i3JzULLEsOWyzIfE8+ECAwEAAaNTMFEwHQYDVR0OBBYEFO2ScoBwXTfoY24DnYubWuYVKONbMB8GA1UdIwQYMBaAFO2ScoBwXTfoY24DnYubWuYVKONbMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBADhbclqOYQwh5cDNq1pdA4XqBNPSGRg9liu84JzuhWnADAO+4fmyr9D1Usr2te1NW5pzGWxcOI8kg05l7ZPut0Vu937aBf9UAR5Q0qK2aV6450kmC4q7+Kw1qEsRbefs/90PFAI7mCnnVAuUCpdZNz8RwONAGsgS43tpfF4ZM5nXYBsFoLP3jQBWeepOx4EkJK+wjywJ55aLpJPznrYH1C1N6Du+Aon1YKkPSzpNEpt2+LBr0844EZOVhrBoL08ClGEgThk6uNycRL0I7nDPddY+27lCqDk5UimMdonvDaXC5K/5Lmf+EWzv3vp6X00e1Wo5YrYZSIv8LLhG9VU8+AfZ1NHmEvEaVz1Dx1H9yl6K9RZ9RRq+bsq6GwCUCqdBkCyFY2jkdpgpcADj1bXTTdKkKEGbq8z5yUKDoXfwmxRJMQmqYh1r61nHG8XLriO8FQc5qUJQpI+ITH8nfqtd4VTDs7QIopWfPihx/r5vk11jB5cGKwGzg5+IlcsQK0OW+se42h0/WNgcMz+rimJzuQta30xPR26McquC8IbrpJhgjwLFEfoZgxHVOGFC9/O8Gc+xi62prly81R1bbhnYCy/BUILEz56yWyXojATpjBacoZTW5EwIHoFjagngi5pOznmzbnbKMhLlioj8IHm/s8YYchBU3mKYufDg+B/jASoI</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://test.keycloak.com/auth/login/epd/callback/soap-5" index="5"/>
|
||||
<md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://test.keycloak.com/auth/login/epd/callback/soap-1" index="1"/>
|
||||
<md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://test.keycloak.com/auth/login/epd/callback/soap-9" index="9"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://test.keycloak.com/auth/login/epd/callback/http-artifact-8" index="8"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://test.keycloak.com/auth/login/epd/callback/http-post" index="2"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://test.keycloak.com/auth/login/epd/callback/http-post-simplesign" index="3"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS" Location="https://test.keycloak.com/auth/login/epd/callback/paos" index="4"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://test.keycloak.com/auth/login/epd/callback/http-artifact-6" index="6"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://test.keycloak.com/auth/login/epd/callback/http-artifact-7" index="7"/>
|
||||
</md:SPSSODescriptor>
|
||||
</md:EntityDescriptor>
|
|
@ -0,0 +1,21 @@
|
|||
<md:EntityDescriptor entityID="https://keycloak.com" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
|
||||
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<md:Extensions>
|
||||
<init:RequestInitiator Binding="urn:oasis:names:tc:SAML:profiles:SSO:request-init" Location="https://test.keycloak.com/auth/login/epd" xmlns:init="urn:oasis:names:tc:SAML:profiles:SSO:request-init"/>
|
||||
</md:Extensions>
|
||||
<md:KeyDescriptor>
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:KeyName>keycloak</ds:KeyName>
|
||||
<ds:X509Data>
|
||||
<ds:X509SubjectName>CN=keycloak.com</ds:X509SubjectName>
|
||||
<ds:X509Certificate>MIIFDzCCAvegAwIBAgIUM+Ho+HIxh9p4yV8qT5S+SsyYlY4wDQYJKoZIhvcNAQELBQAwFzEVMBMGA1UEAwwMa2V5Y2xvYWsuY29tMB4XDTE5MDQwNTEzMjAzNFoXDTI5MDQwMjEzMjAzNFowFzEVMBMGA1UEAwwMa2V5Y2xvYWsuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAu+VA5xDggY+wfkA7LFcmRI45UPpUp47nMKVOF11f4jh7XfjztdOcKILDaYdg1N8Ldm1DUZTi81KkTQdUg8DjyaJj/di12UKVJAusaGxfaF6XfVihHtxckVGAuXU4BbPdQZ+qMiPNw0G/mFIIU+9ykIApjAyU4eHuKmjI83oXzCsN5bxZmzcR5QKDa/AwTQtpaTNd7vStm5mS+lIQIwB3g9vYYIIzasoP/H27MfeAg+7jK9BsKQrfUJ+GYsb5S3NBor6K4laeBslbKJUaBu29ekPqLacrwdaq1TJXpbfSOLYlM0vzJmlN/SVavqM3eBvFzIYD4VKg43JwQd/7W7cGVnoawmbaWMPYvZojeNeU9j94BKNchQX606ROqKuAM50zA6m8b8Y6KwF4zHexHDuZXBYTtk/HsDnrO7Y6Hz0KzEtqj/E5YHukvhSYkKj+DP/8nPnJOCE48tVRqmlhMs8LDR5DA1SI4Z+jAiFEuYa6tMFjUTTYl6O1ZijJCTe+K6p4OgfdEUAA3cwdvsGz6jYrJXB1v9WiZKQRLV6LVkPqH6TUcx/hV3Ca9J9+GkkynMDkKBNo6EZZ/XX5pielc2CtSE4vR78rIzkNMpl5DhOh6iDlrT2dI86soGtdSDXm7DQK0HOjZzFpaDl9Q0Xp3C7wgjl2i3JzULLEsOWyzIfE8+ECAwEAAaNTMFEwHQYDVR0OBBYEFO2ScoBwXTfoY24DnYubWuYVKONbMB8GA1UdIwQYMBaAFO2ScoBwXTfoY24DnYubWuYVKONbMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBADhbclqOYQwh5cDNq1pdA4XqBNPSGRg9liu84JzuhWnADAO+4fmyr9D1Usr2te1NW5pzGWxcOI8kg05l7ZPut0Vu937aBf9UAR5Q0qK2aV6450kmC4q7+Kw1qEsRbefs/90PFAI7mCnnVAuUCpdZNz8RwONAGsgS43tpfF4ZM5nXYBsFoLP3jQBWeepOx4EkJK+wjywJ55aLpJPznrYH1C1N6Du+Aon1YKkPSzpNEpt2+LBr0844EZOVhrBoL08ClGEgThk6uNycRL0I7nDPddY+27lCqDk5UimMdonvDaXC5K/5Lmf+EWzv3vp6X00e1Wo5YrYZSIv8LLhG9VU8+AfZ1NHmEvEaVz1Dx1H9yl6K9RZ9RRq+bsq6GwCUCqdBkCyFY2jkdpgpcADj1bXTTdKkKEGbq8z5yUKDoXfwmxRJMQmqYh1r61nHG8XLriO8FQc5qUJQpI+ITH8nfqtd4VTDs7QIopWfPihx/r5vk11jB5cGKwGzg5+IlcsQK0OW+se42h0/WNgcMz+rimJzuQta30xPR26McquC8IbrpJhgjwLFEfoZgxHVOGFC9/O8Gc+xi62prly81R1bbhnYCy/BUILEz56yWyXojATpjBacoZTW5EwIHoFjagngi5pOznmzbnbKMhLlioj8IHm/s8YYchBU3mKYufDg+B/jASoI</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</md:KeyDescriptor>
|
||||
<md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://test.keycloak.com/auth/login/epd/callback/soap" index="1"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://test.keycloak.com/auth/login/epd/callback/http-artifact" index="1"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://test.keycloak.com/auth/login/epd/callback/http-post" index="2"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://test.keycloak.com/auth/login/epd/callback/http-post-simplesign" index="3"/>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS" Location="https://test.keycloak.com/auth/login/epd/callback/paos" index="4"/>
|
||||
</md:SPSSODescriptor>
|
||||
</md:EntityDescriptor>
|
|
@ -148,7 +148,7 @@ public class ClientSettingsForm extends CreateClientForm {
|
|||
public void setEnabled(boolean enabled) {
|
||||
enabledSwitch.setOn(enabled);
|
||||
}
|
||||
|
||||
|
||||
public boolean isAlwaysDisplayInConsole() {
|
||||
return alwaysDisplayInConsole.isOn();
|
||||
}
|
||||
|
@ -335,4 +335,4 @@ public class ClientSettingsForm extends CreateClientForm {
|
|||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ public class ClientSettingsTest extends AbstractClientTest {
|
|||
private ClientSettings clientSettingsPage;
|
||||
|
||||
private ClientRepresentation newClient;
|
||||
|
||||
|
||||
@Test
|
||||
public void crudOIDCPublic() {
|
||||
newClient = createClientRep("oidc-public", OIDC);
|
||||
|
@ -60,22 +60,22 @@ public class ClientSettingsTest extends AbstractClientTest {
|
|||
ClientRepresentation found = findClientByClientId(newClient.getClientId());
|
||||
assertNotNull("Client " + newClient.getClientId() + " was not found.", found);
|
||||
assertClientSettingsEqual(newClient, found);
|
||||
|
||||
|
||||
// update & verify
|
||||
newClient.setClientId("oidc-public-updated");
|
||||
newClient.setName("updatedName");
|
||||
|
||||
|
||||
List<String> redirectUris = new ArrayList<>();
|
||||
redirectUris.add("http://example2.test/app/*");
|
||||
redirectUris.add("http://example2.test/app2/*");
|
||||
redirectUris.add("http://example3.test/app/*");
|
||||
newClient.setRedirectUris(redirectUris);
|
||||
|
||||
|
||||
List<String> webOrigins = new ArrayList<>();
|
||||
webOrigins.add("http://example2.test");
|
||||
webOrigins.add("http://example3.test");
|
||||
newClient.setWebOrigins(webOrigins);
|
||||
|
||||
|
||||
clientSettingsPage.form().setClientId("oidc-public-updated");
|
||||
clientSettingsPage.form().setName("updatedName");
|
||||
clientSettingsPage.form().setRedirectUris(redirectUris);
|
||||
|
@ -84,7 +84,7 @@ public class ClientSettingsTest extends AbstractClientTest {
|
|||
assertAlertSuccess();
|
||||
|
||||
assertFalse(clientSettingsPage.form().isAlwaysDisplayInConsoleVisible());
|
||||
|
||||
|
||||
found = findClientByClientId(newClient.getClientId());
|
||||
assertNotNull("Client " + newClient.getClientId() + " was not found.", found);
|
||||
assertClientSettingsEqual(newClient, found);
|
||||
|
@ -130,10 +130,10 @@ public class ClientSettingsTest extends AbstractClientTest {
|
|||
public void createOIDCConfidential() {
|
||||
newClient = createClientRep("oidc-confidetial", OIDC);
|
||||
createClient(newClient);
|
||||
|
||||
|
||||
newClient.setRedirectUris(TEST_REDIRECT_URIs);
|
||||
newClient.setPublicClient(false);
|
||||
|
||||
|
||||
clientSettingsPage.form().setAccessType(CONFIDENTIAL);
|
||||
clientSettingsPage.form().setRedirectUris(TEST_REDIRECT_URIs);
|
||||
clientSettingsPage.form().save();
|
||||
|
@ -142,29 +142,29 @@ public class ClientSettingsTest extends AbstractClientTest {
|
|||
assertNotNull("Client " + newClient.getClientId() + " was not found.", found);
|
||||
assertClientSettingsEqual(newClient, found);
|
||||
}
|
||||
|
||||
|
||||
//KEYCLOAK-4022
|
||||
@Test
|
||||
public void testOIDCConfidentialServiceAccountRolesTab() {
|
||||
newClient = createClientRep("oidc-service-account-tab", OIDC);
|
||||
createClient(newClient);
|
||||
|
||||
|
||||
newClient.setRedirectUris(TEST_REDIRECT_URIs);
|
||||
newClient.setPublicClient(false);
|
||||
|
||||
|
||||
clientSettingsPage.form().setAccessType(CONFIDENTIAL);
|
||||
clientSettingsPage.form().setServiceAccountsEnabled(true);
|
||||
assertTrue(clientSettingsPage.form().isServiceAccountsEnabled());
|
||||
//check if Service Account Roles tab is not present
|
||||
assertFalse(clientSettingsPage.tabs().isServiceAccountRolesDisplayed());
|
||||
|
||||
|
||||
clientSettingsPage.form().setRedirectUris(TEST_REDIRECT_URIs);
|
||||
clientSettingsPage.form().save();
|
||||
|
||||
|
||||
//should be there now
|
||||
assertTrue(clientSettingsPage.tabs().getTabs().findElement(By.linkText("Service Account Roles")).isDisplayed());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void saveOIDCConfidentialWithoutRedirectURIs() {
|
||||
newClient = createClientRep("oidc-confidential", OIDC);
|
||||
|
@ -182,10 +182,10 @@ public class ClientSettingsTest extends AbstractClientTest {
|
|||
|
||||
clientSettingsPage.form().setAccessType(BEARER_ONLY);
|
||||
clientSettingsPage.form().save();
|
||||
|
||||
|
||||
newClient.setBearerOnly(true);
|
||||
newClient.setPublicClient(false);
|
||||
|
||||
|
||||
ClientRepresentation found = findClientByClientId(newClient.getClientId());
|
||||
assertNotNull("Client " + newClient.getClientId() + " was not found.", found);
|
||||
assertClientSettingsEqual(newClient, found);
|
||||
|
@ -201,7 +201,7 @@ public class ClientSettingsTest extends AbstractClientTest {
|
|||
assertClientSettingsEqual(newClient, found);
|
||||
assertClientSamlAttributes(getSAMLAttributes(), found.getAttributes());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void invalidSettings() {
|
||||
clientsPage.table().createClient();
|
||||
|
|
|
@ -335,6 +335,8 @@ include-authnstatement=Include AuthnStatement
|
|||
include-authnstatement.tooltip=Should a statement specifying the method and timestamp be included in login responses?
|
||||
include-onetimeuse-condition=Include OneTimeUse Condition
|
||||
include-onetimeuse-condition.tooltip=Should a OneTimeUse Condition be included in login responses?
|
||||
artifact-binding = Force Artifact Binding
|
||||
artifact-binding.tooltip = Should response messages be returned to the client through the SAML ARTIFACT binding system?
|
||||
sign-documents=Sign Documents
|
||||
sign-documents.tooltip=Should SAML documents be signed by the realm?
|
||||
sign-documents-redirect-enable-key-info-ext=Optimize REDIRECT signing key lookup
|
||||
|
@ -407,6 +409,12 @@ logout-service-post-binding-url=Logout Service POST Binding URL
|
|||
logout-service-post-binding-url.tooltip=SAML POST Binding URL for the client's single logout service. You can leave this blank if you are using a different binding
|
||||
logout-service-redir-binding-url=Logout Service Redirect Binding URL
|
||||
logout-service-redir-binding-url.tooltip=SAML Redirect Binding URL for the client's single logout service. You can leave this blank if you are using a different binding.
|
||||
logout-service-artifact-binding-url=Logout Service ARTIFACT Binding URL
|
||||
logout-service-artifact-binding-url.tooltip=SAML ARTIFACT Binding URL for the client's single logout service. You can leave this blank if you are using a different binding.
|
||||
artifact-binding-url= Artifact Binding URL
|
||||
artifact-binding-url.tooltip=URL to send the HTTP ARTIFACT messages to. You can leave this blank if you are using a different binding. This value should be set when forcing ARTIFACT binding together with IdP initiated login.
|
||||
artifact-resolution-service-url= Artifact Resolution Service
|
||||
artifact-resolution-service-url.tooltip= SAML Artifact resolution service for the client. This is the endpoint to which Keycloak will send a SOAP ArtifactResolve mesasge. You can leave this blank if you do not have a URL for this binding.
|
||||
saml-signature-keyName-transformer=SAML Signature Key Name
|
||||
saml-signature-keyName-transformer.tooltip=Signed SAML documents contain identification of signing key in KeyName element. For Keycloak / RH-SSO counterparty, use KEY_ID, for MS AD FS use CERT_SUBJECT, for others check and use NONE if no other option works.
|
||||
oidc-compatibility-modes=OpenID Connect Compatibility Modes
|
||||
|
|
|
@ -1097,6 +1097,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
|
|||
$scope.samlAuthnStatement = false;
|
||||
$scope.samlOneTimeUseCondition = false;
|
||||
$scope.samlMultiValuedRoles = false;
|
||||
$scope.samlArtifactBinding = false;
|
||||
$scope.samlServerSignature = false;
|
||||
$scope.samlServerSignatureEnableKeyInfoExtension = false;
|
||||
$scope.samlAssertionSignature = false;
|
||||
|
@ -1179,6 +1180,16 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
|
|||
} else if ($scope.client.attributes['saml_name_id_format'] == 'persistent') {
|
||||
$scope.nameIdFormat = $scope.nameIdFormats[3];
|
||||
}
|
||||
|
||||
|
||||
if ($scope.client.attributes["saml.artifact.binding"]) {
|
||||
if ($scope.client.attributes["saml.artifact.binding"] == "true") {
|
||||
$scope.samlArtifactBinding = true;
|
||||
} else {
|
||||
$scope.samlArtifactBinding = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($scope.client.attributes["saml.server.signature"]) {
|
||||
if ($scope.client.attributes["saml.server.signature"] == "true") {
|
||||
$scope.samlServerSignature = true;
|
||||
|
@ -1635,6 +1646,11 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
|
|||
}
|
||||
delete $scope.clientEdit.requestUris;
|
||||
|
||||
if ($scope.samlArtifactBinding == true) {
|
||||
$scope.clientEdit.attributes["saml.artifact.binding"] = "true";
|
||||
} else {
|
||||
$scope.clientEdit.attributes["saml.artifact.binding"] = "false";
|
||||
}
|
||||
if ($scope.samlServerSignature == true) {
|
||||
$scope.clientEdit.attributes["saml.server.signature"] = "true";
|
||||
} else {
|
||||
|
|
|
@ -171,6 +171,15 @@
|
|||
</div>
|
||||
<kc-tooltip>{{:: 'include-onetimeuse-condition.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
|
||||
<label class="col-md-2 control-label" for="samlArtifactBinding">{{:: 'artifact-binding' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
<input ng-model="samlArtifactBinding" ng-click="switchChange()" name="samlArtifactBinding" id="samlArtifactBinding" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'artifact-binding.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
|
||||
<label class="col-md-2 control-label" for="samlServerSignature">{{:: 'sign-documents' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
|
@ -416,12 +425,35 @@
|
|||
<kc-tooltip>{{:: 'logout-service-post-binding-url.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
|
||||
<label class="col-md-2 control-label" for="logoutPostBinding">{{:: 'logout-service-redir-binding-url' | translate}}</label>
|
||||
<label class="col-md-2 control-label" for="logoutRedirectBinding">{{:: 'logout-service-redir-binding-url' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
<input ng-model="clientEdit.attributes.saml_single_logout_service_url_redirect" class="form-control" type="text" name="logoutRedirectBinding" id="logoutRedirectBinding" />
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'logout-service-redir-binding-url.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
|
||||
<label class="col-md-2 control-label" for="logoutArtifactBinding">{{:: 'logout-service-artifact-binding-url' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
<input ng-model="clientEdit.attributes.saml_single_logout_service_url_artifact" class="form-control" type="text" name="logoutRedirectBinding" id="logoutArtifactBinding" />
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'logout-service-artifact-binding-url.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
|
||||
<label class="col-md-2 control-label" for="artifactBindingUrl">{{:: 'artifact-binding-url' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
<input ng-model="clientEdit.attributes.saml_artifact_binding_url" class="form-control" type="text" name="artifactBindingUrl" id="artifactBindingUrl" />
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'artifact-binding-url.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
|
||||
<label class="col-md-2 control-label" for="artifactResolutionServiceUrl">{{:: 'artifact-resolution-service-url' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
<input ng-model="clientEdit.attributes.saml_artifact_resolution_service_url" class="form-control" type="text" name="artifactResolutionServiceUrl" id="artifactResolutionServiceUrl" />
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'artifact-resolution-service-url.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset data-ng-show="protocol == 'openid-connect'">
|
||||
|
|
|
@ -367,6 +367,7 @@ openshift.scope.list-projects=List projects
|
|||
saml.post-form.title=Authentication Redirect
|
||||
saml.post-form.message=Redirecting, please wait.
|
||||
saml.post-form.js-disabled=JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue.
|
||||
saml.artifactResolutionServiceInvalidResponse=Unable to resolve artifact.
|
||||
|
||||
#authenticators
|
||||
otp-display-name=Authenticator Application
|
||||
|
|
Loading…
Reference in a new issue