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_RESPONSE_KEY = "SAMLResponse";
|
||||||
|
|
||||||
|
String SAML_ARTIFACT_KEY = "SAMLart";
|
||||||
|
|
||||||
String SAML_SIG_ALG_REQUEST_KEY = "SigAlg";
|
String SAML_SIG_ALG_REQUEST_KEY = "SigAlg";
|
||||||
|
|
||||||
String SAML_SIGNATURE_REQUEST_KEY = "Signature";
|
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_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_SOAP_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:SOAP"),
|
||||||
SAML_PAOS_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:PAOS"),
|
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"),
|
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.Signature;
|
||||||
import java.security.SignatureException;
|
import java.security.SignatureException;
|
||||||
import java.security.cert.X509Certificate;
|
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.common.util.HtmlUtils.escapeAttribute;
|
||||||
import static org.keycloak.saml.common.util.StringUtil.isNotNull;
|
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) {
|
public String buildHtml(String samlResponse, String actionUrl, boolean asRequest) {
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
|
|
||||||
String key = GeneralConstants.SAML_RESPONSE_KEY;
|
String key = GeneralConstants.SAML_RESPONSE_KEY;
|
||||||
|
|
||||||
|
@ -319,32 +320,44 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
|
||||||
key = GeneralConstants.SAML_REQUEST_KEY;
|
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>")
|
builder.append("<HTML>")
|
||||||
.append("<HEAD>")
|
.append("<HEAD>")
|
||||||
|
|
||||||
.append("<TITLE>Authentication Redirect</TITLE>")
|
.append("<TITLE>Authentication Redirect</TITLE>")
|
||||||
.append("</HEAD>")
|
.append("</HEAD>")
|
||||||
.append("<BODY Onload=\"document.forms[0].submit()\">")
|
.append("<BODY Onload=\"document.forms[0].submit()\">")
|
||||||
|
|
||||||
.append("<FORM METHOD=\"POST\" ACTION=\"").append(actionUrl).append("\">")
|
.append("<FORM METHOD=\"POST\" ACTION=\"").append(actionUrl).append("\">");
|
||||||
.append("<INPUT TYPE=\"HIDDEN\" NAME=\"").append(key).append("\"").append(" VALUE=\"").append(samlResponse).append("\"/>");
|
|
||||||
|
|
||||||
builder.append("<p>Redirecting, please wait.</p>");
|
builder.append("<p>Redirecting, please wait.</p>");
|
||||||
|
|
||||||
if (isNotNull(relayState)) {
|
for (String key: inputTypes.keySet()) {
|
||||||
builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"RelayState\" " + "VALUE=\"").append(escapeAttribute(relayState)).append("\"/>");
|
builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"").append(key).append("\"").append(" VALUE=\"").append(escapeAttribute(inputTypes.get(key))).append("\"/>");
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.append("<NOSCRIPT>")
|
builder.append("<NOSCRIPT>")
|
||||||
.append("<P>JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue.</P>")
|
.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("<INPUT TYPE=\"SUBMIT\" VALUE=\"CONTINUE\" />")
|
||||||
.append("</NOSCRIPT>")
|
.append("</NOSCRIPT>")
|
||||||
|
|
||||||
.append("</FORM></BODY></HTML>");
|
.append("</FORM></BODY></HTML>");
|
||||||
|
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public String base64Encoded(Document document) throws ConfigurationException, ProcessingException, IOException {
|
public String base64Encoded(Document document) throws ConfigurationException, ProcessingException, IOException {
|
||||||
String documentAsString = DocumentUtil.getDocumentAsString(document);
|
String documentAsString = DocumentUtil.getDocumentAsString(document);
|
||||||
logger.debugv("saml document: {0}", documentAsString);
|
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();
|
int pos = builder.getQuery() == null? 0 : builder.getQuery().length();
|
||||||
builder.queryParam(samlParameterName, base64Encoded(document));
|
builder.queryParam(samlParameterName, base64Encoded(document));
|
||||||
if (relayState != null) {
|
if (relayState != null) {
|
||||||
builder.queryParam("RelayState", relayState);
|
builder.queryParam(GeneralConstants.RELAY_STATE, relayState);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sign) {
|
if (sign) {
|
||||||
|
|
|
@ -51,11 +51,20 @@ import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_
|
||||||
public class SPMetadataDescriptor {
|
public class SPMetadataDescriptor {
|
||||||
|
|
||||||
public static String getSPDescriptor(URI binding, URI assertionEndpoint, URI logoutEndpoint,
|
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,
|
boolean wantAuthnRequestsSigned, boolean wantAssertionsSigned, boolean wantAssertionsEncrypted,
|
||||||
String entityId, String nameIDPolicyFormat, List<Element> signingCerts, List<Element> encryptionCerts)
|
String entityId, String nameIDPolicyFormat, List<Element> signingCerts, List<Element> encryptionCerts)
|
||||||
throws XMLStreamException, ProcessingException, ParserConfigurationException
|
throws XMLStreamException, ProcessingException, ParserConfigurationException
|
||||||
{
|
{
|
||||||
|
|
||||||
StringWriter sw = new StringWriter();
|
StringWriter sw = new StringWriter();
|
||||||
XMLStreamWriter writer = StaxUtil.getXMLStreamWriter(sw);
|
XMLStreamWriter writer = StaxUtil.getXMLStreamWriter(sw);
|
||||||
SAMLMetadataWriter metadataWriter = new SAMLMetadataWriter(writer);
|
SAMLMetadataWriter metadataWriter = new SAMLMetadataWriter(writer);
|
||||||
|
@ -67,7 +76,7 @@ public class SPMetadataDescriptor {
|
||||||
spSSODescriptor.setAuthnRequestsSigned(wantAuthnRequestsSigned);
|
spSSODescriptor.setAuthnRequestsSigned(wantAuthnRequestsSigned);
|
||||||
spSSODescriptor.setWantAssertionsSigned(wantAssertionsSigned);
|
spSSODescriptor.setWantAssertionsSigned(wantAssertionsSigned);
|
||||||
spSSODescriptor.addNameIDFormat(nameIDPolicyFormat);
|
spSSODescriptor.addNameIDFormat(nameIDPolicyFormat);
|
||||||
spSSODescriptor.addSingleLogoutService(new EndpointType(binding, logoutEndpoint));
|
spSSODescriptor.addSingleLogoutService(new EndpointType(logoutBinding, logoutEndpoint));
|
||||||
|
|
||||||
if (wantAuthnRequestsSigned && signingCerts != null) {
|
if (wantAuthnRequestsSigned && signingCerts != null) {
|
||||||
for (Element key: signingCerts)
|
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.setIsDefault(true);
|
||||||
assertionConsumerEndpoint.setIndex(1);
|
assertionConsumerEndpoint.setIndex(1);
|
||||||
spSSODescriptor.addAssertionConsumerService(assertionConsumerEndpoint);
|
spSSODescriptor.addAssertionConsumerService(assertionConsumerEndpoint);
|
||||||
|
|
|
@ -177,7 +177,17 @@ public class SAML2Request {
|
||||||
throw logger.nullArgumentError("InputStream");
|
throw logger.nullArgumentError("InputStream");
|
||||||
|
|
||||||
Document samlDocument = DocumentUtil.getDocument(is);
|
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();
|
SAMLParser samlParser = SAMLParser.getInstance();
|
||||||
JAXPValidationUtil.checkSchemaValidation(samlDocument);
|
JAXPValidationUtil.checkSchemaValidation(samlDocument);
|
||||||
SAML2Object requestType = (SAML2Object) samlParser.parse(samlDocument);
|
SAML2Object requestType = (SAML2Object) samlParser.parse(samlDocument);
|
||||||
|
|
|
@ -90,6 +90,16 @@ public class SAMLArtifactResponseParser extends SAMLStatusResponseTypeParser<Art
|
||||||
target.setStatus(SAMLStatusParser.getInstance().parse(xmlEventReader));
|
target.setStatus(SAMLStatusParser.getInstance().parse(xmlEventReader));
|
||||||
break;
|
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:
|
default:
|
||||||
throw LOGGER.parserUnknownTag(StaxParserUtil.getElementName(elementDetail), elementDetail.getLocation());
|
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.assertion.NameIDType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
|
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
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.ResponseType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
|
import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.StatusDetailType;
|
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 org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
||||||
import javax.xml.crypto.dsig.XMLSignature;
|
import javax.xml.crypto.dsig.XMLSignature;
|
||||||
|
|
||||||
|
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_NSURI;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write a SAML Response to stream
|
* Write a SAML Response to stream
|
||||||
*
|
*
|
||||||
|
@ -135,9 +138,16 @@ public class SAMLResponseWriter extends BaseWriter {
|
||||||
AuthnRequestType authn = (AuthnRequestType) anyObj;
|
AuthnRequestType authn = (AuthnRequestType) anyObj;
|
||||||
SAMLRequestWriter requestWriter = new SAMLRequestWriter(writer);
|
SAMLRequestWriter requestWriter = new SAMLRequestWriter(writer);
|
||||||
requestWriter.write(authn);
|
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) {
|
} else if (anyObj instanceof ResponseType) {
|
||||||
ResponseType rt = (ResponseType) anyObj;
|
ResponseType rt = (ResponseType) anyObj;
|
||||||
write(rt);
|
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);
|
StaxUtil.writeEndElement(writer);
|
||||||
|
|
|
@ -52,6 +52,8 @@ public interface Errors {
|
||||||
String INVALID_SAML_AUTHN_REQUEST = "invalid_authn_request";
|
String INVALID_SAML_AUTHN_REQUEST = "invalid_authn_request";
|
||||||
String INVALID_SAML_LOGOUT_REQUEST = "invalid_logout_request";
|
String INVALID_SAML_LOGOUT_REQUEST = "invalid_logout_request";
|
||||||
String INVALID_SAML_LOGOUT_RESPONSE = "invalid_logout_response";
|
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 SAML_TOKEN_NOT_FOUND = "saml_token_not_found";
|
||||||
String INVALID_SIGNATURE = "invalid_signature";
|
String INVALID_SIGNATURE = "invalid_signature";
|
||||||
String INVALID_REGISTRATION = "invalid_registration";
|
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.CodeToTokenStoreSpi
|
||||||
org.keycloak.models.OAuth2DeviceTokenStoreSpi
|
org.keycloak.models.OAuth2DeviceTokenStoreSpi
|
||||||
org.keycloak.models.OAuth2DeviceUserCodeSpi
|
org.keycloak.models.OAuth2DeviceUserCodeSpi
|
||||||
|
org.keycloak.models.SamlArtifactSessionMappingStoreSpi
|
||||||
org.keycloak.models.SingleUseTokenStoreSpi
|
org.keycloak.models.SingleUseTokenStoreSpi
|
||||||
org.keycloak.models.TokenRevocationStoreSpi
|
org.keycloak.models.TokenRevocationStoreSpi
|
||||||
org.keycloak.models.UserSessionSpi
|
org.keycloak.models.UserSessionSpi
|
||||||
|
@ -73,6 +74,7 @@ org.keycloak.authorization.store.StoreFactorySpi
|
||||||
org.keycloak.authorization.AuthorizationSpi
|
org.keycloak.authorization.AuthorizationSpi
|
||||||
org.keycloak.models.cache.authorization.CachedStoreFactorySpi
|
org.keycloak.models.cache.authorization.CachedStoreFactorySpi
|
||||||
org.keycloak.protocol.oidc.TokenIntrospectionSpi
|
org.keycloak.protocol.oidc.TokenIntrospectionSpi
|
||||||
|
org.keycloak.protocol.saml.ArtifactResolverSpi
|
||||||
org.keycloak.policy.PasswordPolicySpi
|
org.keycloak.policy.PasswordPolicySpi
|
||||||
org.keycloak.policy.PasswordPolicyManagerSpi
|
org.keycloak.policy.PasswordPolicyManagerSpi
|
||||||
org.keycloak.transaction.TransactionManagerLookupSpi
|
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 {
|
enum State {
|
||||||
LOGGED_IN,
|
LOGGED_IN,
|
||||||
LOGGING_OUT,
|
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.broker.provider.DefaultDataMarshaller;
|
||||||
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
import org.keycloak.dom.saml.v2.assertion.AssertionType;
|
||||||
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
|
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.dom.saml.v2.protocol.ResponseType;
|
||||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||||
|
@ -58,6 +59,10 @@ public class SAMLDataMarshaller extends DefaultDataMarshaller {
|
||||||
AuthnStatementType authnStatement = (AuthnStatementType) obj;
|
AuthnStatementType authnStatement = (AuthnStatementType) obj;
|
||||||
SAMLAssertionWriter samlWriter = new SAMLAssertionWriter(StaxUtil.getXMLStreamWriter(bos));
|
SAMLAssertionWriter samlWriter = new SAMLAssertionWriter(StaxUtil.getXMLStreamWriter(bos));
|
||||||
samlWriter.write(authnStatement, true);
|
samlWriter.write(authnStatement, true);
|
||||||
|
} else if (obj instanceof ArtifactResponseType) {
|
||||||
|
ArtifactResponseType artifactResponseType = (ArtifactResponseType) obj;
|
||||||
|
SAMLResponseWriter samlWriter = new SAMLResponseWriter(StaxUtil.getXMLStreamWriter(bos));
|
||||||
|
samlWriter.write(artifactResponseType);
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Don't know how to serialize object of type " + obj.getClass().getName());
|
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;
|
String xmlString = serialized;
|
||||||
|
|
||||||
try {
|
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);
|
byte[] bytes = xmlString.getBytes(GeneralConstants.SAML_CHARSET);
|
||||||
InputStream is = new ByteArrayInputStream(bytes);
|
InputStream is = new ByteArrayInputStream(bytes);
|
||||||
Object respType = SAMLParser.getInstance().parse(is);
|
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 maxPooledPerRoute = config.getInt("max-pooled-per-route", 64);
|
||||||
int connectionPoolSize = config.getInt("connection-pool-size", 128);
|
int connectionPoolSize = config.getInt("connection-pool-size", 128);
|
||||||
long connectionTTL = config.getLong("connection-ttl-millis", -1L);
|
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);
|
long maxConnectionIdleTime = config.getLong("max-connection-idle-time-millis", 900000L);
|
||||||
boolean disableCookies = config.getBoolean("disable-cookies", true);
|
boolean disableCookies = config.getBoolean("disable-cookies", true);
|
||||||
String clientKeystore = config.get("client-keystore");
|
String clientKeystore = config.get("client-keystore");
|
||||||
|
@ -152,6 +153,7 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
|
||||||
.establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS)
|
.establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS)
|
||||||
.maxPooledPerRoute(maxPooledPerRoute)
|
.maxPooledPerRoute(maxPooledPerRoute)
|
||||||
.connectionPoolSize(connectionPoolSize)
|
.connectionPoolSize(connectionPoolSize)
|
||||||
|
.reuseConnections(reuseConnections)
|
||||||
.connectionTTL(connectionTTL, TimeUnit.MILLISECONDS)
|
.connectionTTL(connectionTTL, TimeUnit.MILLISECONDS)
|
||||||
.maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS)
|
.maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS)
|
||||||
.disableCookies(disableCookies)
|
.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.SSLContexts;
|
||||||
import org.apache.http.conn.ssl.StrictHostnameVerifier;
|
import org.apache.http.conn.ssl.StrictHostnameVerifier;
|
||||||
import org.apache.http.conn.ssl.X509HostnameVerifier;
|
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.CloseableHttpClient;
|
||||||
import org.apache.http.impl.client.HttpClients;
|
import org.apache.http.impl.client.HttpClients;
|
||||||
|
|
||||||
|
@ -95,6 +96,7 @@ public class HttpClientBuilder {
|
||||||
protected int connectionPoolSize = 128;
|
protected int connectionPoolSize = 128;
|
||||||
protected int maxPooledPerRoute = 64;
|
protected int maxPooledPerRoute = 64;
|
||||||
protected long connectionTTL = -1;
|
protected long connectionTTL = -1;
|
||||||
|
protected boolean reuseConnections = true;
|
||||||
protected TimeUnit connectionTTLUnit = TimeUnit.MILLISECONDS;
|
protected TimeUnit connectionTTLUnit = TimeUnit.MILLISECONDS;
|
||||||
protected long maxConnectionIdleTime = 900000;
|
protected long maxConnectionIdleTime = 900000;
|
||||||
protected TimeUnit maxConnectionIdleTimeUnit = TimeUnit.MILLISECONDS;
|
protected TimeUnit maxConnectionIdleTimeUnit = TimeUnit.MILLISECONDS;
|
||||||
|
@ -140,6 +142,11 @@ public class HttpClientBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public HttpClientBuilder reuseConnections(boolean reuseConnections) {
|
||||||
|
this.reuseConnections = reuseConnections;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public HttpClientBuilder maxConnectionIdleTime(long maxConnectionIdleTime, TimeUnit unit) {
|
public HttpClientBuilder maxConnectionIdleTime(long maxConnectionIdleTime, TimeUnit unit) {
|
||||||
this.maxConnectionIdleTime = maxConnectionIdleTime;
|
this.maxConnectionIdleTime = maxConnectionIdleTime;
|
||||||
this.maxConnectionIdleTimeUnit = unit;
|
this.maxConnectionIdleTimeUnit = unit;
|
||||||
|
@ -289,6 +296,9 @@ public class HttpClientBuilder {
|
||||||
.setMaxConnPerRoute(maxPooledPerRoute)
|
.setMaxConnPerRoute(maxPooledPerRoute)
|
||||||
.setConnectionTimeToLive(connectionTTL, connectionTTLUnit);
|
.setConnectionTimeToLive(connectionTTL, connectionTTLUnit);
|
||||||
|
|
||||||
|
if (!reuseConnections) {
|
||||||
|
builder.setConnectionReuseStrategy(new NoConnectionReuseStrategy());
|
||||||
|
}
|
||||||
|
|
||||||
if (proxyMappings != null && !proxyMappings.isEmpty()) {
|
if (proxyMappings != null && !proxyMappings.isEmpty()) {
|
||||||
builder.setRoutePlanner(new ProxyMappingsAwareRoutePlanner(proxyMappings));
|
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;
|
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) {
|
private static ClientRepresentation loadEntityDescriptors(InputStream is) {
|
||||||
Object metadata;
|
Object metadata;
|
||||||
try {
|
try {
|
||||||
|
@ -172,6 +212,16 @@ public class EntityDescriptorDescriptionConverter implements ClientDescriptionCo
|
||||||
if (assertionConsumerServicePaosBinding != null) {
|
if (assertionConsumerServicePaosBinding != null) {
|
||||||
redirectUris.add(assertionConsumerServicePaosBinding);
|
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) {
|
if (spDescriptorType.getNameIDFormat() != null) {
|
||||||
for (String format : spDescriptorType.getNameIDFormat()) {
|
for (String format : spDescriptorType.getNameIDFormat()) {
|
||||||
String attribute = SamlClient.samlNameIDFormatToClientAttribute(format);
|
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.EntitiesDescriptorType;
|
||||||
import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType;
|
import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType;
|
||||||
import org.keycloak.dom.saml.v2.metadata.IDPSSODescriptorType;
|
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.KeyDescriptorType;
|
||||||
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
|
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.Document;
|
||||||
import org.w3c.dom.Element;
|
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_POST_BINDING;
|
||||||
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING;
|
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING;
|
||||||
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.SAML_SOAP_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 class IDPMetadataDescriptor {
|
||||||
|
|
||||||
public static String getIDPDescriptor(URI loginPostEndpoint, URI loginRedirectEndpoint, URI logoutEndpoint,
|
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
|
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_POST_BINDING.getUri(), logoutEndpoint));
|
||||||
spIDPDescriptor.addSingleLogoutService(new EndpointType(SAML_HTTP_REDIRECT_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_POST_BINDING.getUri(), loginPostEndpoint));
|
||||||
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_HTTP_REDIRECT_BINDING.getUri(), loginRedirectEndpoint));
|
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_HTTP_REDIRECT_BINDING.getUri(), loginRedirectEndpoint));
|
||||||
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_SOAP_BINDING.getUri(), loginPostEndpoint));
|
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) {
|
if (wantAuthnRequestsSigned && signingCerts != null) {
|
||||||
for (Element key: signingCerts)
|
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));
|
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() {
|
public boolean requiresRealmSignature() {
|
||||||
return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE));
|
return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE));
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ public interface SamlConfigAttributes {
|
||||||
String SAML_AUTHNSTATEMENT = "saml.authnstatement";
|
String SAML_AUTHNSTATEMENT = "saml.authnstatement";
|
||||||
String SAML_ONETIMEUSE_CONDITION = "saml.onetimeuse.condition";
|
String SAML_ONETIMEUSE_CONDITION = "saml.onetimeuse.condition";
|
||||||
String SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE = "saml_force_name_id_format";
|
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 = "saml.server.signature";
|
||||||
String SAML_SERVER_SIGNATURE_KEYINFO_EXT = "saml.server.signature.keyinfo.ext";
|
String SAML_SERVER_SIGNATURE_KEYINFO_EXT = "saml.server.signature.keyinfo.ext";
|
||||||
String SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER = "saml.server.signature.keyinfo.xmlSigKeyInfoKeyNameTransformer";
|
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.client.methods.HttpPost;
|
||||||
import org.apache.http.message.BasicNameValuePair;
|
import org.apache.http.message.BasicNameValuePair;
|
||||||
import org.jboss.logging.Logger;
|
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.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.AssertionType;
|
||||||
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
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.LogoutRequestType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
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.Details;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
|
@ -38,6 +43,7 @@ import org.keycloak.models.KeyManager;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.ProtocolMapperModel;
|
import org.keycloak.models.ProtocolMapperModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.SamlArtifactSessionMappingStoreProvider;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.protocol.LoginProtocol;
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
|
@ -51,6 +57,7 @@ import org.keycloak.saml.SAML2ErrorResponseBuilder;
|
||||||
import org.keycloak.saml.SAML2LoginResponseBuilder;
|
import org.keycloak.saml.SAML2LoginResponseBuilder;
|
||||||
import org.keycloak.saml.SAML2LogoutRequestBuilder;
|
import org.keycloak.saml.SAML2LogoutRequestBuilder;
|
||||||
import org.keycloak.saml.SAML2LogoutResponseBuilder;
|
import org.keycloak.saml.SAML2LogoutResponseBuilder;
|
||||||
|
import org.keycloak.saml.SAML2NameIDBuilder;
|
||||||
import org.keycloak.saml.SamlProtocolExtensionsAwareBuilder.NodeGenerator;
|
import org.keycloak.saml.SamlProtocolExtensionsAwareBuilder.NodeGenerator;
|
||||||
import org.keycloak.saml.SignatureAlgorithm;
|
import org.keycloak.saml.SignatureAlgorithm;
|
||||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
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.managers.ResourceAdminManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.resources.RealmsResource;
|
import org.keycloak.services.resources.RealmsResource;
|
||||||
import org.keycloak.sessions.CommonClientSessionModel;
|
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
import org.keycloak.sessions.CommonClientSessionModel;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
@ -96,14 +104,15 @@ import org.apache.http.util.EntityUtils;
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class SamlProtocol implements LoginProtocol {
|
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_TRUE_VALUE = "true";
|
||||||
public static final String ATTRIBUTE_FALSE_VALUE = "false";
|
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_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_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_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_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 LOGIN_PROTOCOL = "saml";
|
||||||
public static final String SAML_BINDING = "saml_binding";
|
public static final String SAML_BINDING = "saml_binding";
|
||||||
public static final String SAML_IDP_INITIATED_LOGIN = "saml_idp_initiated_login";
|
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_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_LOGIN_REQUEST_FORCEAUTHN = "SAML_LOGIN_REQUEST_FORCEAUTHN";
|
||||||
public static final String SAML_FORCEAUTHN_REQUIREMENT = "true";
|
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;
|
protected KeycloakSession session;
|
||||||
|
|
||||||
|
@ -138,6 +150,9 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
|
|
||||||
protected EventBuilder event;
|
protected EventBuilder event;
|
||||||
|
|
||||||
|
protected ArtifactResolver artifactResolver;
|
||||||
|
protected SamlArtifactSessionMappingStoreProvider artifactSessionMappingStore;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SamlProtocol setSession(KeycloakSession session) {
|
public SamlProtocol setSession(KeycloakSession session) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
|
@ -168,6 +183,20 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
return this;
|
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
|
@Override
|
||||||
public Response sendError(AuthenticationSessionModel authSession, Error error) {
|
public Response sendError(AuthenticationSessionModel authSession, Error error) {
|
||||||
try {
|
try {
|
||||||
|
@ -187,8 +216,8 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return samlErrorMessage(
|
return samlErrorMessage(
|
||||||
authSession, new SamlClient(client), isPostBinding(authSession),
|
authSession, new SamlClient(client), isPostBinding(authSession),
|
||||||
authSession.getRedirectUri(), translateErrorToSAMLStatus(error), authSession.getClientNote(GeneralConstants.RELAY_STATE)
|
authSession.getRedirectUri(), translateErrorToSAMLStatus(error), authSession.getClientNote(GeneralConstants.RELAY_STATE)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -197,8 +226,8 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Response samlErrorMessage(
|
private Response samlErrorMessage(
|
||||||
AuthenticationSessionModel authSession, SamlClient samlClient, boolean isPostBinding,
|
AuthenticationSessionModel authSession, SamlClient samlClient, boolean isPostBinding,
|
||||||
String destination, JBossSAMLURIConstants statusDetail, String relayState) {
|
String destination, JBossSAMLURIConstants statusDetail, String relayState) {
|
||||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(session).relayState(relayState);
|
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(session).relayState(relayState);
|
||||||
SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(destination).issuer(getResponseIssuer(realm)).status(statusDetail.get());
|
SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(destination).issuer(getResponseIssuer(realm)).status(statusDetail.get());
|
||||||
KeyManager keyManager = session.keys();
|
KeyManager keyManager = session.keys();
|
||||||
|
@ -312,13 +341,13 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
String configuredNameIdFormat = samlClient.getNameIDFormat();
|
String configuredNameIdFormat = samlClient.getNameIDFormat();
|
||||||
if ((nameIdFormat == null || forceFormat) && configuredNameIdFormat != null) {
|
if ((nameIdFormat == null || forceFormat) && configuredNameIdFormat != null) {
|
||||||
nameIdFormat = configuredNameIdFormat;
|
nameIdFormat = configuredNameIdFormat;
|
||||||
}
|
}
|
||||||
if (nameIdFormat == null)
|
if (nameIdFormat == null)
|
||||||
return SAML_DEFAULT_NAMEID_FORMAT;
|
return SAML_DEFAULT_NAMEID_FORMAT;
|
||||||
return nameIdFormat;
|
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())) {
|
if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) {
|
||||||
final String email = userSession.getUser().getEmail();
|
final String email = userSession.getUser().getEmail();
|
||||||
if (email == null) {
|
if (email == null) {
|
||||||
|
@ -342,11 +371,11 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
* Attempts to retrieve the persistent type NameId as follows:
|
* Attempts to retrieve the persistent type NameId as follows:
|
||||||
*
|
*
|
||||||
* <ol>
|
* <ol>
|
||||||
* <li>saml.persistent.name.id.for.$clientId user attribute</li>
|
* <li>saml.persistent.name.id.for.$clientId user attribute</li>
|
||||||
* <li>saml.persistent.name.id.for.* user attribute</li>
|
* <li>saml.persistent.name.id.for.* user attribute</li>
|
||||||
* <li>G-$randomUuid</li>
|
* <li>G-$randomUuid</li>
|
||||||
* </ol>
|
* </ol>
|
||||||
*
|
* <p>
|
||||||
* If a randomUuid is generated, an attribute for the given saml.persistent.name.id.for.$clientId will be generated,
|
* 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.
|
* 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) {
|
if (nameId == null) {
|
||||||
return samlErrorMessage(
|
return samlErrorMessage(
|
||||||
null, samlClient, isPostBinding(authSession),
|
null, samlClient, isPostBinding(authSession),
|
||||||
redirectUri, JBossSAMLURIConstants.STATUS_INVALID_NAMEIDPOLICY, relayState
|
redirectUri, JBossSAMLURIConstants.STATUS_INVALID_NAMEIDPOLICY, relayState
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -418,7 +447,7 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
builder.disableAuthnStatement(true);
|
builder.disableAuthnStatement(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.includeOneTimeUseCondition(samlClient.includeOneTimeUseCondition());
|
builder.includeOneTimeUseCondition(samlClient.includeOneTimeUseCondition());
|
||||||
|
|
||||||
List<ProtocolMapperProcessor<SAMLAttributeStatementMapper>> attributeStatementMappers = new LinkedList<>();
|
List<ProtocolMapperProcessor<SAMLAttributeStatementMapper>> attributeStatementMappers = new LinkedList<>();
|
||||||
List<ProtocolMapperProcessor<SAMLLoginResponseMapper>> loginResponseMappers = new LinkedList<>();
|
List<ProtocolMapperProcessor<SAMLLoginResponseMapper>> loginResponseMappers = new LinkedList<>();
|
||||||
|
@ -441,17 +470,18 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
});
|
});
|
||||||
|
|
||||||
Document samlDocument = null;
|
Document samlDocument = null;
|
||||||
|
ResponseType samlModel = null;
|
||||||
KeyManager keyManager = session.keys();
|
KeyManager keyManager = session.keys();
|
||||||
KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm);
|
KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm);
|
||||||
boolean postBinding = isPostBinding(authSession);
|
boolean postBinding = isPostBinding(authSession);
|
||||||
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
|
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ((! postBinding) && samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
|
if ((!postBinding) && samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
|
||||||
builder.addExtension(new KeycloakKeySamlExtensionGenerator(keyName));
|
builder.addExtension(new KeycloakKeySamlExtensionGenerator(keyName));
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponseType samlModel = builder.buildModel();
|
samlModel = builder.buildModel();
|
||||||
final AttributeStatementType attributeStatement = populateAttributeStatements(attributeStatementMappers, session, userSession, clientSession);
|
final AttributeStatementType attributeStatement = populateAttributeStatements(attributeStatementMappers, session, userSession, clientSession);
|
||||||
populateRoles(roleListMapper.get(), session, userSession, clientSessionCtx, attributeStatement);
|
populateRoles(roleListMapper.get(), session, userSession, clientSessionCtx, attributeStatement);
|
||||||
|
|
||||||
|
@ -462,7 +492,6 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
samlModel = transformLoginResponse(loginResponseMappers, samlModel, session, userSession, clientSessionCtx);
|
samlModel = transformLoginResponse(loginResponseMappers, samlModel, session, userSession, clientSessionCtx);
|
||||||
samlDocument = builder.buildDocument(samlModel);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("failed", e);
|
logger.error("failed", e);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.FAILED_TO_PROCESS_RESPONSE);
|
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);
|
JaxrsSAML2BindingBuilder bindingBuilder = new JaxrsSAML2BindingBuilder(session);
|
||||||
bindingBuilder.relayState(relayState);
|
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();
|
String canonicalization = samlClient.getCanonicalizationMethod();
|
||||||
if (canonicalization != null) {
|
if (canonicalization != null) {
|
||||||
bindingBuilder.canonicalizationMethod(canonicalization);
|
bindingBuilder.canonicalizationMethod(canonicalization);
|
||||||
}
|
}
|
||||||
bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
|
bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate());
|
||||||
}
|
|
||||||
if (samlClient.requiresAssertionSignature()) {
|
if (samlClient.requiresRealmSignature()) bindingBuilder.signDocument();
|
||||||
String canonicalization = samlClient.getCanonicalizationMethod();
|
if (samlClient.requiresAssertionSignature()) bindingBuilder.signAssertions();
|
||||||
if (canonicalization != null) {
|
|
||||||
bindingBuilder.canonicalizationMethod(canonicalization);
|
|
||||||
}
|
|
||||||
bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signAssertions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (samlClient.requiresEncryption()) {
|
if (samlClient.requiresEncryption()) {
|
||||||
PublicKey publicKey = null;
|
PublicKey publicKey = null;
|
||||||
try {
|
try {
|
||||||
|
@ -496,6 +531,7 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
bindingBuilder.encrypt(publicKey);
|
bindingBuilder.encrypt(publicKey);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
samlDocument = builder.buildDocument(samlModel);
|
||||||
return buildAuthenticatedResponse(clientSession, redirectUri, samlDocument, bindingBuilder);
|
return buildAuthenticatedResponse(clientSession, redirectUri, samlDocument, bindingBuilder);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("failed", e);
|
logger.error("failed", e);
|
||||||
|
@ -551,19 +587,27 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
roleListMapper.mapper.mapRoles(existingAttributeStatement, roleListMapper.model, session, userSession, clientSessionCtx);
|
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;
|
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);
|
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE);
|
||||||
} else {
|
} else {
|
||||||
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE);
|
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logoutServiceUrl == null)
|
if (logoutServiceUrl == null)
|
||||||
logoutServiceUrl = client.getManagementUrl();
|
logoutServiceUrl = client.getManagementUrl();
|
||||||
if (logoutServiceUrl == null || logoutServiceUrl.trim().equals(""))
|
if (logoutServiceUrl == null || logoutServiceUrl.trim().equals(""))
|
||||||
return null;
|
return null;
|
||||||
return ResourceAdminManager.resolveUri(session, client.getRootUrl(), logoutServiceUrl);
|
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
|
@Override
|
||||||
|
@ -572,41 +616,41 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
SamlClient samlClient = new SamlClient(client);
|
SamlClient samlClient = new SamlClient(client);
|
||||||
try {
|
try {
|
||||||
boolean postBinding = isLogoutPostBindingForClient(clientSession);
|
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) {
|
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());
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postBinding) {
|
NodeGenerator[] extensions = new NodeGenerator[]{};
|
||||||
LogoutRequestType logoutRequest = createLogoutRequest(bindingUri, clientSession, client);
|
if (!postBinding) {
|
||||||
// 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;
|
|
||||||
if (samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
|
if (samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
|
||||||
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
|
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
|
||||||
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
|
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
|
||||||
extensions = new NodeGenerator[] { new KeycloakKeySamlExtensionGenerator(keyName) };
|
extensions = new NodeGenerator[]{new KeycloakKeySamlExtensionGenerator(keyName)};
|
||||||
} else {
|
|
||||||
extensions = new NodeGenerator[] {};
|
|
||||||
}
|
}
|
||||||
LogoutRequestType logoutRequest = createLogoutRequest(bindingUri, clientSession, client, extensions);
|
|
||||||
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
|
|
||||||
return binding.redirectBinding(SAML2Request.convert(logoutRequest)).request(bindingUri);
|
|
||||||
}
|
}
|
||||||
} catch (ConfigurationException e) {
|
LogoutRequestType logoutRequest = createLogoutRequest(bindingUri, clientSession, client, extensions);
|
||||||
throw new RuntimeException(e);
|
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient, "true".equals(clientSession.getNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get())));
|
||||||
} catch (ProcessingException e) {
|
|
||||||
throw new RuntimeException(e);
|
//If this session uses artifact binding, send an artifact instead of the LogoutRequest
|
||||||
} catch (IOException e) {
|
if ("true".equals(clientSession.getNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get()))
|
||||||
throw new RuntimeException(e);
|
&& useArtifactForLogout(client)) {
|
||||||
} catch (ParsingException e) {
|
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);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -635,11 +679,11 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
}
|
}
|
||||||
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
|
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
|
||||||
XmlKeyInfoKeyNameTransformer transformer = XmlKeyInfoKeyNameTransformer.from(
|
XmlKeyInfoKeyNameTransformer transformer = XmlKeyInfoKeyNameTransformer.from(
|
||||||
userSession.getNote(SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER),
|
userSession.getNote(SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER),
|
||||||
SamlClient.DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER);
|
SamlClient.DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER);
|
||||||
String keyName = transformer.getKeyName(keys.getKid(), keys.getCertificate());
|
String keyName = transformer.getKeyName(keys.getKid(), keys.getCertificate());
|
||||||
binding.signatureAlgorithm(algorithm).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
|
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
|
if (addExtension) { // Only include extension if REDIRECT binding and signing whole SAML protocol message
|
||||||
builder.addExtension(new KeycloakKeySamlExtensionGenerator(keyName));
|
builder.addExtension(new KeycloakKeySamlExtensionGenerator(keyName));
|
||||||
}
|
}
|
||||||
|
@ -647,7 +691,7 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
Response response;
|
Response response;
|
||||||
try {
|
try {
|
||||||
response = buildLogoutResponse(userSession, logoutBindingUri, builder, binding);
|
response = buildLogoutResponse(userSession, logoutBindingUri, builder, binding);
|
||||||
} catch (ConfigurationException | ProcessingException | IOException e) {
|
} catch (ConfigurationException | ProcessingException | IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
if (logoutBindingUri != null) {
|
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 {
|
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)) {
|
if (isLogoutPostBindingForInitiator(userSession)) {
|
||||||
return binding.postBinding(builder.buildDocument()).response(logoutBindingUri);
|
return binding.postBinding(samlDocument).response(logoutBindingUri);
|
||||||
} else {
|
} 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) {
|
public Response backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
|
||||||
ClientModel client = clientSession.getClient();
|
ClientModel client = clientSession.getClient();
|
||||||
SamlClient samlClient = new SamlClient(client);
|
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) {
|
if (logoutUrl == null) {
|
||||||
logger.warnf("Can't do backchannel logout. No SingleLogoutService POST Binding registered for client: %s",
|
logger.warnf("Can't do backchannel logout. No SingleLogoutService POST Binding registered for client: %s",
|
||||||
client.getClientId());
|
client.getClientId());
|
||||||
|
@ -687,7 +739,7 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
String logoutRequestString = null;
|
String logoutRequestString = null;
|
||||||
try {
|
try {
|
||||||
LogoutRequestType logoutRequest = createLogoutRequest(logoutUrl, clientSession, client);
|
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
|
// 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();
|
logoutRequestString = binding.postBinding(SAML2Request.convert(logoutRequest)).encoded();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -701,8 +753,8 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
|
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
|
||||||
formparams.add(new BasicNameValuePair(GeneralConstants.SAML_REQUEST_KEY, logoutRequestString));
|
formparams.add(new BasicNameValuePair(GeneralConstants.SAML_REQUEST_KEY, logoutRequestString));
|
||||||
formparams.add(new BasicNameValuePair("BACK_CHANNEL_LOGOUT", "BACK_CHANNEL_LOGOUT")); // for Picketlink
|
formparams.add(new BasicNameValuePair("BACK_CHANNEL_LOGOUT", "BACK_CHANNEL_LOGOUT")); // for Picketlink
|
||||||
// todo remove
|
// todo remove
|
||||||
// this
|
// this
|
||||||
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
|
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
|
||||||
HttpPost post = new HttpPost(logoutUrl);
|
HttpPost post = new HttpPost(logoutUrl);
|
||||||
post.setEntity(form);
|
post.setEntity(form);
|
||||||
|
@ -742,10 +794,10 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
logoutBuilder.addExtension(extension);
|
logoutBuilder.addExtension(extension);
|
||||||
}
|
}
|
||||||
LogoutRequestType logoutRequest = logoutBuilder.createLogoutRequest();
|
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);
|
logoutRequest = it.next().beforeSendingLogoutRequest(logoutRequest, clientSession.getUserSession(), clientSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
return logoutRequest;
|
return logoutRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -755,9 +807,9 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
return Objects.equals(SamlProtocol.SAML_FORCEAUTHN_REQUIREMENT, requireReauthentication);
|
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);
|
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(session);
|
||||||
if (samlClient.requiresRealmSignature()) {
|
if (!skipRealmSignature && samlClient.requiresRealmSignature()) {
|
||||||
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
|
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
|
||||||
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
|
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
|
||||||
binding.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
|
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() {
|
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;
|
package org.keycloak.protocol.saml;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.net.URI;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
|
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.common.util.PemUtils;
|
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.models.ClientModel;
|
||||||
import org.keycloak.saml.SignatureAlgorithm;
|
import org.keycloak.saml.SignatureAlgorithm;
|
||||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
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.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.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.keycloak.saml.processing.web.util.RedirectBindingUtil;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
|
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.UriInfo;
|
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.SAML2Object;
|
||||||
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
|
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.rotation.KeyLocator;
|
||||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||||
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
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.CertificateException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
import org.w3c.dom.Element;
|
import org.w3c.dom.Element;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -198,4 +218,77 @@ public class SamlProtocolUtils {
|
||||||
|
|
||||||
return null;
|
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);
|
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() {
|
public String getSamlServerSignature() {
|
||||||
if (getAttributes() == null) return null;
|
if (getAttributes() == null) return null;
|
||||||
return getAttributes().get(SamlConfigAttributes.SAML_SERVER_SIGNATURE);
|
return getAttributes().get(SamlConfigAttributes.SAML_SERVER_SIGNATURE);
|
||||||
|
|
|
@ -17,85 +17,134 @@
|
||||||
|
|
||||||
package org.keycloak.protocol.saml;
|
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.logging.Logger;
|
||||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
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.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||||
|
import org.keycloak.broker.saml.SAMLDataMarshaller;
|
||||||
|
import org.keycloak.common.ClientConnection;
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.common.util.PemUtils;
|
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.KeyStatus;
|
||||||
|
import org.keycloak.crypto.KeyUse;
|
||||||
|
import org.keycloak.crypto.KeyWrapper;
|
||||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||||
import org.keycloak.dom.saml.v2.assertion.BaseIDAbstractType;
|
import org.keycloak.dom.saml.v2.assertion.BaseIDAbstractType;
|
||||||
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
import org.keycloak.dom.saml.v2.assertion.NameIDType;
|
||||||
import org.keycloak.dom.saml.v2.assertion.SubjectType;
|
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.AuthnRequestType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.NameIDPolicyType;
|
import org.keycloak.dom.saml.v2.protocol.NameIDPolicyType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
|
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.dom.saml.v2.protocol.StatusResponseType;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.executors.ExecutorsProvider;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeyManager;
|
import org.keycloak.models.KeyManager;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakTransaction;
|
||||||
import org.keycloak.models.KeycloakUriInfo;
|
import org.keycloak.models.KeycloakUriInfo;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.SamlArtifactSessionMappingModel;
|
||||||
|
import org.keycloak.models.SamlArtifactSessionMappingStoreProvider;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.protocol.AuthorizationEndpointBase;
|
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.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||||
import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor;
|
import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor;
|
||||||
import org.keycloak.protocol.saml.profile.ecp.SamlEcpProfileService;
|
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.SAML2LogoutResponseBuilder;
|
||||||
|
import org.keycloak.saml.SAML2NameIDBuilder;
|
||||||
import org.keycloak.saml.SAMLRequestParser;
|
import org.keycloak.saml.SAMLRequestParser;
|
||||||
import org.keycloak.saml.SignatureAlgorithm;
|
import org.keycloak.saml.SignatureAlgorithm;
|
||||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
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.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.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.ErrorPage;
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
|
import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.resources.RealmsResource;
|
import org.keycloak.services.resources.RealmsResource;
|
||||||
|
import org.keycloak.services.scheduled.ScheduledTaskRunner;
|
||||||
import org.keycloak.services.util.CacheControlUtil;
|
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.keycloak.utils.MediaType;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
import org.w3c.dom.Element;
|
import org.w3c.dom.Element;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.FormParam;
|
import javax.ws.rs.FormParam;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.NotFoundException;
|
||||||
import javax.ws.rs.POST;
|
import javax.ws.rs.POST;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.PathParam;
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.container.AsyncResponse;
|
||||||
import javax.ws.rs.core.UriInfo;
|
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.io.InputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.stream.Collectors;
|
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.ws.rs.core.MultivaluedMap;
|
||||||
import javax.xml.crypto.dsig.XMLSignature;
|
|
||||||
import javax.xml.parsers.ParserConfigurationException;
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
|
|
||||||
import org.w3c.dom.Document;
|
import static org.keycloak.common.util.StackUtil.getShortStackTrace;
|
||||||
import org.w3c.dom.NodeList;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resource class for the saml connect token service
|
* Resource class for the saml connect token service
|
||||||
|
@ -106,6 +155,7 @@ import org.w3c.dom.NodeList;
|
||||||
public class SamlService extends AuthorizationEndpointBase {
|
public class SamlService extends AuthorizationEndpointBase {
|
||||||
|
|
||||||
protected static final Logger logger = Logger.getLogger(SamlService.class);
|
protected static final Logger logger = Logger.getLogger(SamlService.class);
|
||||||
|
public static final String ARTIFACT_RESOLUTION_SERVICE_PATH = "resolve";
|
||||||
|
|
||||||
private final DestinationValidator destinationValidator;
|
private final DestinationValidator destinationValidator;
|
||||||
|
|
||||||
|
@ -121,7 +171,8 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
// and we want to turn it off.
|
// and we want to turn it off.
|
||||||
protected boolean redirectToAuthentication;
|
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()) {
|
if (!checkSsl()) {
|
||||||
event.event(EventType.LOGIN);
|
event.event(EventType.LOGIN);
|
||||||
event.error(Errors.SSL_REQUIRED);
|
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);
|
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.event(EventType.LOGIN);
|
||||||
event.error(Errors.SAML_TOKEN_NOT_FOUND);
|
event.error(Errors.SAML_TOKEN_NOT_FOUND);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
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);
|
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
SAML2Object samlObject = documentHolder.getSamlObject();
|
SAML2Object samlObject = documentHolder.getSamlObject();
|
||||||
|
|
||||||
if (samlObject instanceof AuthnRequestType) {
|
if (samlObject instanceof AuthnRequestType) {
|
||||||
|
@ -236,27 +288,9 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
String issuer = requestAbstractType.getIssuer() == null ? null : issuerNameId.getValue();
|
String issuer = requestAbstractType.getIssuer() == null ? null : issuerNameId.getValue();
|
||||||
ClientModel client = realm.getClientByClientId(issuer);
|
ClientModel client = realm.getClientByClientId(issuer);
|
||||||
|
|
||||||
if (client == null) {
|
Response error = checkClientValidity(client);
|
||||||
event.client(issuer);
|
if (error != null) {
|
||||||
event.error(Errors.CLIENT_NOT_FOUND);
|
return error;
|
||||||
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.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
session.getContext().setClient(client);
|
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 void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException;
|
||||||
|
|
||||||
protected abstract boolean containsUnencryptedSignature(SAMLDocumentHolder documentHolder);
|
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
|
if (redirectUri != null && ! "null".equals(redirectUri.toString())) { // "null" is for testing purposes
|
||||||
redirect = RedirectUtils.verifyRedirectUri(session, redirectUri.toString(), client);
|
redirect = RedirectUtils.verifyRedirectUri(session, redirectUri.toString(), client);
|
||||||
} else {
|
} 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);
|
redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE);
|
||||||
} else {
|
} else {
|
||||||
redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE);
|
redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE);
|
||||||
|
@ -333,6 +445,14 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = createAuthenticationSession(client, relayState);
|
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.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
|
||||||
authSession.setRedirectUri(redirect);
|
authSession.setRedirectUri(redirect);
|
||||||
authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
||||||
|
@ -365,15 +485,13 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
NameIDType nameID = (NameIDType) baseID;
|
NameIDType nameID = (NameIDType) baseID;
|
||||||
authSession.setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, nameID.getValue());
|
authSession.setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, nameID.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null != requestAbstractType.isForceAuthn()
|
if (null != requestAbstractType.isForceAuthn()
|
||||||
&& requestAbstractType.isForceAuthn()) {
|
&& requestAbstractType.isForceAuthn()) {
|
||||||
authSession.setAuthNote(SamlProtocol.SAML_LOGIN_REQUEST_FORCEAUTHN, SamlProtocol.SAML_FORCEAUTHN_REQUIREMENT);
|
authSession.setAuthNote(SamlProtocol.SAML_LOGIN_REQUEST_FORCEAUTHN, SamlProtocol.SAML_FORCEAUTHN_REQUIREMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
for(Iterator<SamlAuthenticationPreprocessor> it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext();) {
|
for(Iterator<SamlAuthenticationPreprocessor> it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext();) {
|
||||||
requestAbstractType = it.next().beforeProcessingLoginRequest(requestAbstractType, authSession);
|
requestAbstractType = it.next().beforeProcessingLoginRequest(requestAbstractType, authSession);
|
||||||
|
@ -390,6 +508,8 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
if (requestedProtocolBinding != null) {
|
if (requestedProtocolBinding != null) {
|
||||||
if (JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get().equals(requestedProtocolBinding.toString())) {
|
if (JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get().equals(requestedProtocolBinding.toString())) {
|
||||||
return SamlProtocol.SAML_POST_BINDING;
|
return SamlProtocol.SAML_POST_BINDING;
|
||||||
|
} else if (JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get().equals(requestedProtocolBinding.toString())) {
|
||||||
|
return getBindingType();
|
||||||
} else {
|
} else {
|
||||||
return SamlProtocol.SAML_REDIRECT_BINDING;
|
return SamlProtocol.SAML_REDIRECT_BINDING;
|
||||||
}
|
}
|
||||||
|
@ -418,12 +538,12 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, false);
|
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, false);
|
||||||
if (authResult != null) {
|
if (authResult != null) {
|
||||||
String logoutBinding = getBindingType();
|
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())
|
if (samlClient.forcePostBinding() && postBindingUri != null && ! postBindingUri.trim().isEmpty())
|
||||||
logoutBinding = SamlProtocol.SAML_POST_BINDING;
|
logoutBinding = SamlProtocol.SAML_POST_BINDING;
|
||||||
boolean postBinding = Objects.equals(SamlProtocol.SAML_POST_BINDING, logoutBinding);
|
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();
|
UserSessionModel userSession = authResult.getSession();
|
||||||
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING_URI, bindingUri);
|
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING_URI, bindingUri);
|
||||||
if (samlClient.requiresRealmSignature()) {
|
if (samlClient.requiresRealmSignature()) {
|
||||||
|
@ -432,6 +552,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
}
|
}
|
||||||
if (relayState != null)
|
if (relayState != null)
|
||||||
userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState);
|
userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState);
|
||||||
|
|
||||||
userSession.setNote(SamlProtocol.SAML_LOGOUT_REQUEST_ID, logoutRequest.getID());
|
userSession.setNote(SamlProtocol.SAML_LOGOUT_REQUEST_ID, logoutRequest.getID());
|
||||||
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING, logoutBinding);
|
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING, logoutBinding);
|
||||||
userSession.setNote(SamlProtocol.SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO, Boolean.toString((! postBinding) && samlClient.addExtensionsElementWithKeyInfo()));
|
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());
|
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
|
||||||
if (clientSession != null) {
|
if (clientSession != null) {
|
||||||
clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name());
|
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();) {
|
for(Iterator<SamlAuthenticationPreprocessor> it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext();) {
|
||||||
logoutRequest = it.next().beforeProcessingLogoutRequest(logoutRequest, userSession, clientSession);
|
logoutRequest = it.next().beforeProcessingLogoutRequest(logoutRequest, userSession, clientSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("browser Logout");
|
logger.debug("browser Logout");
|
||||||
return authManager.browserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers, null);
|
return authManager.browserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers, null);
|
||||||
} else if (logoutRequest.getSessionIndex() != null) {
|
} else if (logoutRequest.getSessionIndex() != null) {
|
||||||
|
@ -477,9 +607,8 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
// default
|
// default
|
||||||
|
|
||||||
String logoutBinding = getBindingType();
|
String logoutBinding = getBindingType();
|
||||||
String logoutBindingUri = SamlProtocol.getLogoutServiceUrl(session, client, logoutBinding);
|
String logoutBindingUri = SamlProtocol.getLogoutServiceUrl(session, client, logoutBinding, true);
|
||||||
String logoutRelayState = relayState;
|
String logoutRelayState = relayState;
|
||||||
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
|
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
|
||||||
builder.logoutRequestID(logoutRequest.getID());
|
builder.logoutRequestID(logoutRequest.getID());
|
||||||
|
@ -532,16 +661,37 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Response execute(String samlRequest, String samlResponse, String relayState) {
|
public Response execute(String samlRequest, String samlResponse, String relayState, String artifact) {
|
||||||
Response response = basicChecks(samlRequest, samlResponse);
|
Response response = basicChecks(samlRequest, samlResponse, artifact);
|
||||||
if (response != null)
|
if (response != null)
|
||||||
return response;
|
return response;
|
||||||
|
|
||||||
if (samlRequest != null)
|
if (samlRequest != null)
|
||||||
return handleSamlRequest(samlRequest, relayState);
|
return handleSamlRequest(samlRequest, relayState);
|
||||||
else
|
else
|
||||||
return handleSamlResponse(samlResponse, relayState);
|
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.
|
* 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 {
|
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
|
@Override
|
||||||
protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException {
|
protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException {
|
||||||
SamlProtocolUtils.verifyDocumentSignature(client, documentHolder.getSamlDocument());
|
SamlProtocolUtils.verifyDocumentSignature(client, documentHolder.getSamlDocument());
|
||||||
|
@ -588,6 +747,15 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
|
|
||||||
protected class RedirectBindingProtocol extends BindingProtocol {
|
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
|
@Override
|
||||||
protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException {
|
protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException {
|
||||||
PublicKey publicKey = SamlProtocolUtils.getSignatureValidationKey(client);
|
PublicKey publicKey = SamlProtocolUtils.getSignatureValidationKey(client);
|
||||||
|
@ -629,13 +797,22 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
return handleBrowserAuthenticationRequest(authSession, samlProtocol, isPassive, redirectToAuthentication);
|
return handleBrowserAuthenticationRequest(authSession, samlProtocol, isPassive, redirectToAuthentication);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RedirectBindingProtocol newRedirectBindingProtocol() {
|
||||||
|
return new RedirectBindingProtocol();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PostBindingProtocol newPostBindingProtocol() {
|
||||||
|
return new PostBindingProtocol();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
@GET
|
@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");
|
logger.debug("SAML GET");
|
||||||
CacheControlUtil.noBackButtonCacheControlHeader();
|
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
|
@POST
|
||||||
@NoCache
|
@NoCache
|
||||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
@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");
|
logger.debug("SAML POST");
|
||||||
PostBindingProtocol postBindingProtocol = new PostBindingProtocol();
|
PostBindingProtocol postBindingProtocol = new PostBindingProtocol();
|
||||||
// this is to support back button on browser
|
// this is to support back button on browser
|
||||||
// if true, we redirect to authenticate URL otherwise back button behavior has bad side effects
|
// if true, we redirect to authenticate URL otherwise back button behavior has bad side effects
|
||||||
// and we want to turn it off.
|
// and we want to turn it off.
|
||||||
postBindingProtocol.redirectToAuthentication = true;
|
postBindingProtocol.redirectToAuthentication = true;
|
||||||
return postBindingProtocol.execute(samlRequest, samlResponse, relayState);
|
postBindingProtocol.execute(asyncResponse, samlRequest, samlResponse, relayState, artifact);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@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).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(),
|
RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString(),
|
||||||
true,
|
true,
|
||||||
signingKeys);
|
signingKeys);
|
||||||
|
@ -703,6 +882,37 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
return false;
|
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
|
@GET
|
||||||
@Path("clients/{client}")
|
@Path("clients/{client}")
|
||||||
@Produces(MediaType.TEXT_HTML_UTF_8)
|
@Produces(MediaType.TEXT_HTML_UTF_8)
|
||||||
|
@ -802,7 +1012,62 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
return authSession;
|
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
|
@POST
|
||||||
@NoCache
|
@NoCache
|
||||||
@Consumes({"application/soap+xml",MediaType.TEXT_XML})
|
@Consumes({"application/soap+xml",MediaType.TEXT_XML})
|
||||||
|
@ -813,4 +1078,373 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
|
|
||||||
return bindingService.authenticate(inputStream);
|
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);
|
SamlClient samlClient = new SamlClient(client);
|
||||||
String assertionUrl;
|
String assertionUrl;
|
||||||
String logoutUrl;
|
String logoutUrl;
|
||||||
URI binding;
|
URI loginBinding;
|
||||||
|
URI logoutBinding = null;
|
||||||
|
|
||||||
if (samlClient.forcePostBinding()) {
|
if (samlClient.forcePostBinding()) {
|
||||||
assertionUrl = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE);
|
assertionUrl = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE);
|
||||||
logoutUrl = client.getAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_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
|
} else { //redirect binding
|
||||||
assertionUrl = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE);
|
assertionUrl = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE);
|
||||||
logoutUrl = client.getAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_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 = client.getManagementUrl();
|
||||||
if (assertionUrl == null || assertionUrl.trim().isEmpty()) assertionUrl = FALLBACK_ERROR_URL_STRING;
|
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 = client.getManagementUrl();
|
||||||
if (logoutUrl == null || logoutUrl.trim().isEmpty()) logoutUrl = FALLBACK_ERROR_URL_STRING;
|
if (logoutUrl == null || logoutUrl.trim().isEmpty()) logoutUrl = FALLBACK_ERROR_URL_STRING;
|
||||||
|
if (logoutBinding == null) logoutBinding = loginBinding;
|
||||||
|
|
||||||
String nameIdFormat = samlClient.getNameIDFormat();
|
String nameIdFormat = samlClient.getNameIDFormat();
|
||||||
if (nameIdFormat == null) nameIdFormat = SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT;
|
if (nameIdFormat == null) nameIdFormat = SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT;
|
||||||
Element spCertificate = SPMetadataDescriptor.buildKeyInfoElement(null, samlClient.getClientSigningCertificate());
|
Element spCertificate = SPMetadataDescriptor.buildKeyInfoElement(null, samlClient.getClientSigningCertificate());
|
||||||
Element encCertificate = SPMetadataDescriptor.buildKeyInfoElement(null, samlClient.getClientEncryptingCertificate());
|
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(),
|
samlClient.requiresAssertionSignature(), samlClient.requiresEncryption(),
|
||||||
client.getClientId(), nameIdFormat, Arrays.asList(spCertificate), Arrays.asList(encCertificate));
|
client.getClientId(), nameIdFormat, Arrays.asList(spCertificate), Arrays.asList(encCertificate));
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
|
|
@ -29,7 +29,7 @@ import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder;
|
||||||
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
||||||
import org.keycloak.protocol.saml.SamlProtocol;
|
import org.keycloak.protocol.saml.SamlProtocol;
|
||||||
import org.keycloak.protocol.saml.SamlService;
|
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.SAML2LogoutResponseBuilder;
|
||||||
import org.keycloak.saml.common.constants.JBossSAMLConstants;
|
import org.keycloak.saml.common.constants.JBossSAMLConstants;
|
||||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
|
@ -61,6 +61,10 @@ public class SamlEcpProfileService extends SamlService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Response authenticate(InputStream inputStream) {
|
public Response authenticate(InputStream inputStream) {
|
||||||
|
return authenticate(Soap.extractSoapMessage(inputStream));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Response authenticate(Document soapMessage) {
|
||||||
try {
|
try {
|
||||||
return new PostBindingProtocol() {
|
return new PostBindingProtocol() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -80,7 +84,7 @@ public class SamlEcpProfileService extends SamlService {
|
||||||
requestAbstractType.setDestination(session.getContext().getUri().getAbsolutePath());
|
requestAbstractType.setDestination(session.getContext().getUri().getAbsolutePath());
|
||||||
return super.loginRequest(relayState, requestAbstractType, client);
|
return super.loginRequest(relayState, requestAbstractType, client);
|
||||||
}
|
}
|
||||||
}.execute(Soap.toSamlHttpPostMessage(inputStream), null, null);
|
}.execute(Soap.toSamlHttpPostMessage(soapMessage), null, null, null);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
String reason = "Some error occurred while processing the AuthnRequest.";
|
String reason = "Some error occurred while processing the AuthnRequest.";
|
||||||
String detail = e.getMessage();
|
String detail = e.getMessage();
|
||||||
|
|
|
@ -15,18 +15,24 @@
|
||||||
* limitations under the License.
|
* 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.core.saml.v2.util.DocumentUtil;
|
||||||
import org.keycloak.saml.processing.web.util.PostBindingUtil;
|
import org.keycloak.saml.processing.web.util.PostBindingUtil;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
import org.w3c.dom.Node;
|
import org.w3c.dom.Node;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.Response.Status;
|
import javax.ws.rs.core.Response.Status;
|
||||||
import javax.xml.soap.MessageFactory;
|
import javax.xml.soap.MessageFactory;
|
||||||
import javax.xml.soap.Name;
|
import javax.xml.soap.Name;
|
||||||
import javax.xml.soap.SOAPBody;
|
import javax.xml.soap.SOAPBody;
|
||||||
|
import javax.xml.soap.SOAPConnection;
|
||||||
|
import javax.xml.soap.SOAPConnectionFactory;
|
||||||
import javax.xml.soap.SOAPEnvelope;
|
import javax.xml.soap.SOAPEnvelope;
|
||||||
import javax.xml.soap.SOAPException;
|
import javax.xml.soap.SOAPException;
|
||||||
import javax.xml.soap.SOAPFault;
|
import javax.xml.soap.SOAPFault;
|
||||||
|
@ -34,6 +40,7 @@ import javax.xml.soap.SOAPHeaderElement;
|
||||||
import javax.xml.soap.SOAPMessage;
|
import javax.xml.soap.SOAPMessage;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @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.
|
* <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
|
* @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 {
|
try {
|
||||||
MessageFactory messageFactory = MessageFactory.newInstance();
|
MessageFactory messageFactory = MessageFactory.newInstance();
|
||||||
SOAPMessage soapMessage = messageFactory.createMessage(null, inputStream);
|
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();
|
SOAPBody soapBody = soapMessage.getSOAPBody();
|
||||||
Node authnRequestNode = soapBody.getFirstChild();
|
Node authnRequestNode = soapBody.getFirstChild();
|
||||||
Document document = DocumentUtil.createDocument();
|
Document document = DocumentUtil.createDocument();
|
||||||
|
|
||||||
document.appendChild(document.importNode(authnRequestNode, true));
|
document.appendChild(document.importNode(authnRequestNode, true));
|
||||||
|
return document;
|
||||||
return PostBindingUtil.base64Encode(DocumentUtil.asString(document));
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Error creating fault message.", e);
|
throw new RuntimeException("Error creating fault message.", e);
|
||||||
}
|
}
|
||||||
|
@ -123,11 +159,7 @@ public final class Soap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Response build() {
|
public byte[] getBytes() {
|
||||||
return build(Status.OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
Response build(Status status) {
|
|
||||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -135,11 +167,55 @@ public final class Soap {
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Error while building SOAP Fault.", e);
|
throw new RuntimeException("Error while building SOAP Fault.", e);
|
||||||
}
|
}
|
||||||
|
return outputStream.toByteArray();
|
||||||
return Response.status(status).entity(outputStream.toByteArray()).build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
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.OIDCAdvancedConfigWrapper;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.TokenManager;
|
import org.keycloak.protocol.oidc.TokenManager;
|
||||||
|
import org.keycloak.protocol.saml.SamlClient;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.services.ServicesLogger;
|
import org.keycloak.services.ServicesLogger;
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
|
@ -387,6 +388,7 @@ public class AuthenticationManager {
|
||||||
Set<AuthenticatedClientSessionModel> notLoggedOutSessions = acs.entrySet().stream()
|
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, getClientLogoutAction(logoutAuthSession, me.getKey())))
|
||||||
.filter(me -> ! Objects.equals(AuthenticationSessionModel.Action.LOGGED_OUT.name(), me.getValue().getAction()))
|
.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
|
.filter(me -> Objects.nonNull(me.getValue().getProtocol())) // Keycloak service-like accounts
|
||||||
.map(Map.Entry::getValue)
|
.map(Map.Entry::getValue)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
@ -473,7 +475,7 @@ public class AuthenticationManager {
|
||||||
UserSessionModel userSession = clientSession.getUserSession();
|
UserSessionModel userSession = clientSession.getUserSession();
|
||||||
ClientModel client = clientSession.getClient();
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -500,7 +502,9 @@ public class AuthenticationManager {
|
||||||
logger.debug("returning frontchannel logout request to client");
|
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
|
// 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;
|
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) {
|
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()
|
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)
|
.filter(clientSession -> clientSession.getProtocol() != null)
|
||||||
.collect(Collectors.partitioningBy(clientSession -> clientSession.getClient().isFrontchannelLogout()));
|
.collect(Collectors.partitioningBy(clientSession -> clientSession.getClient().isFrontchannelLogout()));
|
||||||
|
|
||||||
|
@ -623,9 +628,10 @@ public class AuthenticationManager {
|
||||||
|
|
||||||
checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession);
|
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);
|
expireIdentityCookie(realm, uriInfo, connection);
|
||||||
expireRememberMeCookie(realm, uriInfo, connection);
|
expireRememberMeCookie(realm, uriInfo, connection);
|
||||||
userSession.setState(UserSessionModel.State.LOGGED_OUT);
|
|
||||||
String method = userSession.getNote(KEYCLOAK_LOGOUT_PROTOCOL);
|
String method = userSession.getNote(KEYCLOAK_LOGOUT_PROTOCOL);
|
||||||
EventBuilder event = new EventBuilder(realm, session, connection);
|
EventBuilder event = new EventBuilder(realm, session, connection);
|
||||||
LoginProtocol protocol = session.getProvider(LoginProtocol.class, method);
|
LoginProtocol protocol = session.getProvider(LoginProtocol.class, method);
|
||||||
|
@ -633,11 +639,50 @@ public class AuthenticationManager {
|
||||||
.setHttpHeaders(headers)
|
.setHttpHeaders(headers)
|
||||||
.setUriInfo(uriInfo)
|
.setUriInfo(uriInfo)
|
||||||
.setEventBuilder(event);
|
.setEventBuilder(event);
|
||||||
|
|
||||||
|
|
||||||
Response response = protocol.finishLogout(userSession);
|
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());
|
session.authenticationSessions().removeRootAuthenticationSession(realm, logoutAuthSession.getParentSession());
|
||||||
|
|
||||||
return response;
|
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) {
|
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 = "delegationFailedMessage";
|
||||||
public static final String DELEGATION_FAILED_HEADER = "delegationFailedHeader";
|
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
|
// WebAuthn
|
||||||
public static final String WEBAUTHN_REGISTER_TITLE = "webauthn-registration-title";
|
public static final String WEBAUTHN_REGISTER_TITLE = "webauthn-registration-title";
|
||||||
public static final String WEBAUTHN_LOGIN_TITLE = "webauthn-login-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/: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=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])
|
/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
|
# Truststore Provider
|
||||||
spi.truststore.file.file=${kc.home.dir}/conf/keycloak.truststore
|
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>
|
<groupId>org.wildfly.core</groupId>
|
||||||
<artifactId>wildfly-controller</artifactId>
|
<artifactId>wildfly-controller</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<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.common.util.StringPropertyReplacer;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.services.error.KeycloakErrorHandler;
|
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.UncaughtServerErrorExpected;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.EnableVault;
|
import org.keycloak.testsuite.arquillian.annotation.EnableVault;
|
||||||
import org.keycloak.testsuite.client.KeycloakTestingClient;
|
import org.keycloak.testsuite.client.KeycloakTestingClient;
|
||||||
import org.keycloak.testsuite.util.LogChecker;
|
import org.keycloak.testsuite.util.LogChecker;
|
||||||
import org.keycloak.testsuite.util.OAuthClient;
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
import org.keycloak.testsuite.util.SpiProvidersSwitchingUtils;
|
||||||
import org.keycloak.testsuite.util.SqlUtils;
|
import org.keycloak.testsuite.util.SqlUtils;
|
||||||
import org.keycloak.testsuite.util.SystemInfoHelper;
|
import org.keycloak.testsuite.util.SystemInfoHelper;
|
||||||
import org.keycloak.testsuite.util.VaultUtils;
|
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) {
|
private ContainerInfo updateWithAuthServerInfo(ContainerInfo authServerInfo) {
|
||||||
return updateWithAuthServerInfo(authServerInfo, 0);
|
return updateWithAuthServerInfo(authServerInfo, 0);
|
||||||
}
|
}
|
||||||
|
@ -531,10 +546,22 @@ public class AuthServerTestEnricher {
|
||||||
TestContext testContext = new TestContext(suiteContext, event.getTestClass().getJavaClass());
|
TestContext testContext = new TestContext(suiteContext, event.getTestClass().getJavaClass());
|
||||||
testContextProducer.set(testContext);
|
testContextProducer.set(testContext);
|
||||||
|
|
||||||
if (!isAuthServerRemote() && !isAuthServerQuarkus() && event.getTestClass().isAnnotationPresent(EnableVault.class)) {
|
if (!isAuthServerRemote() && !isAuthServerQuarkus()) {
|
||||||
VaultUtils.enableVault(suiteContext, event.getTestClass().getAnnotation(EnableVault.class).providerId());
|
boolean wasUpdated = false;
|
||||||
restartAuthServer();
|
|
||||||
testContext.reconnectAdminClient();
|
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);
|
removeTestRealms(testContext, adminClient);
|
||||||
|
|
||||||
if (!isAuthServerRemote() && event.getTestClass().isAnnotationPresent(EnableVault.class)) {
|
if (!isAuthServerRemote() && !isAuthServerQuarkus()) {
|
||||||
VaultUtils.disableVault(suiteContext, event.getTestClass().getAnnotation(EnableVault.class).providerId());
|
|
||||||
restartAuthServer();
|
boolean wasUpdated = false;
|
||||||
testContext.reconnectAdminClient();
|
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) {
|
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);
|
rep.setDefaultDefaultClientScopes(defaultClientScopes);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RealmAttributeUpdater setAccessCodeLifespan(Integer accessCodeLifespan) {
|
||||||
|
rep.setAccessCodeLifespan(accessCodeLifespan);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public RealmAttributeUpdater setSsoSessionIdleTimeout(Integer timeout) {
|
public RealmAttributeUpdater setSsoSessionIdleTimeout(Integer timeout) {
|
||||||
rep.setSsoSessionIdleTimeout(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.impl.client.LaxRedirectStrategy;
|
||||||
import org.apache.http.message.BasicNameValuePair;
|
import org.apache.http.message.BasicNameValuePair;
|
||||||
import org.apache.http.util.EntityUtils;
|
import org.apache.http.util.EntityUtils;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
import org.jsoup.Jsoup;
|
import org.jsoup.Jsoup;
|
||||||
import org.jsoup.nodes.Element;
|
import org.jsoup.nodes.Element;
|
||||||
import org.jsoup.select.Elements;
|
import org.jsoup.select.Elements;
|
||||||
import org.keycloak.adapters.saml.SamlDeployment;
|
import org.keycloak.adapters.saml.SamlDeployment;
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.common.util.KeyUtils;
|
import org.keycloak.common.util.KeyUtils;
|
||||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
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.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.BaseSAML2BindingBuilder;
|
||||||
import org.keycloak.saml.SAMLRequestParser;
|
import org.keycloak.saml.SAMLRequestParser;
|
||||||
import org.keycloak.saml.SignatureAlgorithm;
|
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.ConfigurationException;
|
||||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
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.api.saml.v2.request.SAML2Request;
|
||||||
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
|
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.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.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.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.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.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
|
@ -68,29 +89,15 @@ import java.util.Arrays;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import javax.ws.rs.core.MultivaluedHashMap;
|
import java.util.concurrent.TimeUnit;
|
||||||
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 org.jboss.logging.Logger;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.hamcrest.Matchers.*;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.junit.Assert.assertThat;
|
import static org.junit.Assert.assertThat;
|
||||||
import static org.junit.Assert.assertTrue;
|
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 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.Matchers.statusCodeIsHC;
|
||||||
|
import static org.keycloak.testsuite.util.SamlUtils.getSamlDeploymentForClient;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author hmlnarik
|
* @author hmlnarik
|
||||||
|
@ -446,7 +453,7 @@ public class SamlClient {
|
||||||
envelope.addNamespaceDeclaration(NS_PREFIX_PAOS_BINDING, JBossSAMLURIConstants.PAOS_BINDING.get());
|
envelope.addNamespaceDeclaration(NS_PREFIX_PAOS_BINDING, JBossSAMLURIConstants.PAOS_BINDING.get());
|
||||||
envelope.addNamespaceDeclaration(NS_PREFIX_PROFILE_ECP, JBossSAMLURIConstants.ECP_PROFILE.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);
|
createPaosRequestHeader(envelope, deployment);
|
||||||
createEcpRequestHeader(envelope, deployment);
|
createEcpRequestHeader(envelope, deployment);
|
||||||
|
@ -490,8 +497,88 @@ public class SamlClient {
|
||||||
public String extractRelayState(CloseableHttpResponse response) throws IOException {
|
public String extractRelayState(CloseableHttpResponse response) throws IOException {
|
||||||
return null;
|
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;
|
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());
|
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);
|
currentResponse = client.execute(request, context);
|
||||||
|
|
||||||
|
if (s instanceof StepWithCheckers) {
|
||||||
|
Runnable afterChecker = ((StepWithCheckers) s).getAfterStepChecker();
|
||||||
|
if (afterChecker != null) afterChecker.run();
|
||||||
|
}
|
||||||
|
|
||||||
currentUri = request.getURI();
|
currentUri = request.getURI();
|
||||||
List<URI> locations = context.getRedirectLocations();
|
List<URI> locations = context.getRedirectLocations();
|
||||||
if (locations != null && ! locations.isEmpty()) {
|
if (locations != null && ! locations.isEmpty()) {
|
||||||
|
@ -716,6 +814,8 @@ public class SamlClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected HttpClientBuilder createHttpClientBuilderInstance() {
|
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.dom.saml.v2.SAML2Object;
|
||||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
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.page.AbstractPage;
|
||||||
import org.keycloak.testsuite.util.SamlClient.Binding;
|
import org.keycloak.testsuite.util.SamlClient.Binding;
|
||||||
import org.keycloak.testsuite.util.SamlClient.DoNotFollowRedirectStep;
|
import org.keycloak.testsuite.util.SamlClient.DoNotFollowRedirectStep;
|
||||||
|
@ -29,8 +30,10 @@ import java.util.List;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
import org.apache.http.client.methods.HttpGet;
|
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.CreateAuthnRequestStepBuilder;
|
||||||
import org.keycloak.testsuite.util.saml.CreateLogoutRequestStepBuilder;
|
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.IdPInitiatedLoginBuilder;
|
||||||
import org.keycloak.testsuite.util.saml.LoginBuilder;
|
import org.keycloak.testsuite.util.saml.LoginBuilder;
|
||||||
import org.keycloak.testsuite.util.saml.UpdateProfileBuilder;
|
import org.keycloak.testsuite.util.saml.UpdateProfileBuilder;
|
||||||
|
@ -105,7 +108,6 @@ public class SamlClientBuilder {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a single generic step
|
* Adds a single generic step
|
||||||
* @param step
|
|
||||||
* @return This builder
|
* @return This builder
|
||||||
*/
|
*/
|
||||||
public SamlClientBuilder addStep(Runnable stepWithNoParameters) {
|
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;
|
package org.keycloak.testsuite.util;
|
||||||
|
|
||||||
|
import org.apache.tools.ant.filters.StringInputStream;
|
||||||
import org.keycloak.adapters.saml.SamlDeployment;
|
import org.keycloak.adapters.saml.SamlDeployment;
|
||||||
import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
|
import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
|
||||||
import org.keycloak.adapters.saml.config.parsers.ResourceLoader;
|
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.common.exceptions.ParsingException;
|
||||||
|
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
|
||||||
import org.keycloak.testsuite.utils.arquillian.DeploymentArchiveProcessorUtils;
|
import org.keycloak.testsuite.utils.arquillian.DeploymentArchiveProcessorUtils;
|
||||||
import org.keycloak.testsuite.utils.io.IOUtil;
|
import org.keycloak.testsuite.utils.io.IOUtil;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
|
@ -32,4 +39,16 @@ public class SamlUtils {
|
||||||
};
|
};
|
||||||
return new DeploymentBuilder().build(isProcessed, loader);
|
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 signingPublicKeyPem; // TODO: should not be needed
|
||||||
private String signingPrivateKeyPem;
|
private String signingPrivateKeyPem;
|
||||||
private String signingCertificate;
|
private String signingCertificate;
|
||||||
|
private URI protocolBinding;
|
||||||
private String authorizationHeader;
|
private String authorizationHeader;
|
||||||
|
|
||||||
private final Document forceLoginRequestDocument;
|
private final Document forceLoginRequestDocument;
|
||||||
|
@ -86,6 +87,15 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CreateAuthnRequestStepBuilder setProtocolBinding(URI protocolBinding) {
|
||||||
|
this.protocolBinding = protocolBinding;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public URI getProtocolBinding() {
|
||||||
|
return protocolBinding;
|
||||||
|
}
|
||||||
|
|
||||||
public CreateAuthnRequestStepBuilder signWith(String signingPrivateKeyPem, String signingPublicKeyPem) {
|
public CreateAuthnRequestStepBuilder signWith(String signingPrivateKeyPem, String signingPublicKeyPem) {
|
||||||
return signWith(signingPrivateKeyPem, signingPublicKeyPem, null);
|
return signWith(signingPrivateKeyPem, signingPublicKeyPem, null);
|
||||||
}
|
}
|
||||||
|
@ -96,7 +106,7 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
|
||||||
this.signingCertificate = signingCertificate;
|
this.signingCertificate = signingCertificate;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CreateAuthnRequestStepBuilder basicAuthentication(UserRepresentation user) {
|
public CreateAuthnRequestStepBuilder basicAuthentication(UserRepresentation user) {
|
||||||
String username = user.getUsername();
|
String username = user.getUsername();
|
||||||
String password = Users.getPasswordOf(user);
|
String password = Users.getPasswordOf(user);
|
||||||
|
@ -126,7 +136,7 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
|
||||||
if (authorizationHeader != null) {
|
if (authorizationHeader != null) {
|
||||||
request.addHeader(HttpHeaders.AUTHORIZATION, authorizationHeader);
|
request.addHeader(HttpHeaders.AUTHORIZATION, authorizationHeader);
|
||||||
}
|
}
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,9 +147,10 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SAML2Request samlReq = new SAML2Request();
|
SAML2Request samlReq = new SAML2Request();
|
||||||
AuthnRequestType loginReq = samlReq.createAuthnRequestType(UUID.randomUUID().toString(),
|
AuthnRequestType loginReq = samlReq.createAuthnRequestType(UUID.randomUUID().toString(), assertionConsumerURL, this.authServerSamlUrl.toString(), issuer, requestBinding.getBindingUri());
|
||||||
assertionConsumerURL, this.authServerSamlUrl.toString(), issuer, requestBinding.getBindingUri());
|
if (protocolBinding != null) {
|
||||||
|
loginReq.setProtocolBinding(protocolBinding);
|
||||||
|
}
|
||||||
return SAML2Request.convert(loginReq);
|
return SAML2Request.convert(loginReq);
|
||||||
} catch (ConfigurationException | ParsingException | ProcessingException ex) {
|
} catch (ConfigurationException | ParsingException | ProcessingException ex) {
|
||||||
throw new RuntimeException(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;
|
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.testsuite.util.SamlClientBuilder;
|
||||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||||
|
@ -81,6 +83,9 @@ public class ModifySamlResponseStepBuilder extends SamlDocumentStepBuilder<SAML2
|
||||||
|
|
||||||
case POST:
|
case POST:
|
||||||
return handlePostBinding(currentResponse);
|
return handlePostBinding(currentResponse);
|
||||||
|
|
||||||
|
case ARTIFACT_RESPONSE:
|
||||||
|
return handleArtifactResponse(currentResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new RuntimeException("Unknown binding for " + ModifySamlResponseStepBuilder.class.getName());
|
throw new RuntimeException("Unknown binding for " + ModifySamlResponseStepBuilder.class.getName());
|
||||||
|
@ -130,6 +135,18 @@ public class ModifySamlResponseStepBuilder extends SamlDocumentStepBuilder<SAML2
|
||||||
return this;
|
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 {
|
protected HttpUriRequest handleRedirectBinding(CloseableHttpResponse currentResponse) throws Exception, IOException, URISyntaxException {
|
||||||
String samlDoc;
|
String samlDoc;
|
||||||
final String attrName;
|
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 beforeAbstractKeycloakTestRealmImport() throws Exception {
|
||||||
}
|
}
|
||||||
protected void postAfterAbstractKeycloak() {
|
protected void postAfterAbstractKeycloak() throws Exception {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void afterAbstractKeycloakTestRealmImport() {}
|
protected void afterAbstractKeycloakTestRealmImport() {}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
public void afterAbstractKeycloakTest() {
|
public void afterAbstractKeycloakTest() throws Exception {
|
||||||
if (resetTimeOffset) {
|
if (resetTimeOffset) {
|
||||||
resetTimeOffset();
|
resetTimeOffset();
|
||||||
}
|
}
|
||||||
|
|
|
@ -582,8 +582,9 @@ public class RealmTest extends AbstractAdminTest {
|
||||||
|
|
||||||
ClientRepresentation converted = realm.convertClientDescription(description);
|
ClientRepresentation converted = realm.convertClientDescription(description);
|
||||||
assertEquals("loadbalancer-9.siroe.com", converted.getClientId());
|
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(0));
|
||||||
|
assertEquals("https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp", converted.getRedirectUris().get(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void assertRealm(RealmRepresentation realm, RealmRepresentation storedRealm) {
|
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/post",
|
||||||
"https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/soap",
|
"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/paos",
|
||||||
"https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/redirect"
|
"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/artifact"
|
||||||
|
));
|
||||||
|
|
||||||
assertThat(response.getAttributes().get("saml_single_logout_service_url_redirect"), is("https://LoadBalancer-9.siroe.com:3443/federation/SPSloRedirect/metaAlias/sp"));
|
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
|
@After
|
||||||
@Override
|
@Override
|
||||||
public void afterAbstractKeycloakTest() {
|
public void afterAbstractKeycloakTest() throws Exception {
|
||||||
log.debug("--DC: after AbstractCrossDCTest");
|
log.debug("--DC: after AbstractCrossDCTest");
|
||||||
CrossDCTestEnricher.startAuthServerBackendNode(DC.FIRST, 0); // make sure first node is started
|
CrossDCTestEnricher.startAuthServerBackendNode(DC.FIRST, 0); // make sure first node is started
|
||||||
enableOnlyFirstNodeInFirstDc();
|
enableOnlyFirstNodeInFirstDc();
|
||||||
|
|
|
@ -128,7 +128,7 @@ public class DockerClientTest extends AbstractKeycloakTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterAbstractKeycloakTest() {
|
public void afterAbstractKeycloakTest() throws Exception {
|
||||||
super.afterAbstractKeycloakTest();
|
super.afterAbstractKeycloakTest();
|
||||||
|
|
||||||
pause(5000); // wait for the container logs
|
pause(5000); // wait for the container logs
|
||||||
|
|
|
@ -162,7 +162,7 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@Override
|
@Override
|
||||||
public void afterAbstractKeycloakTest() {
|
public void afterAbstractKeycloakTest() throws Exception {
|
||||||
cleanupApacheHttpClient();
|
cleanupApacheHttpClient();
|
||||||
|
|
||||||
super.afterAbstractKeycloakTest();
|
super.afterAbstractKeycloakTest();
|
||||||
|
|
|
@ -80,7 +80,7 @@ public class OAuthRedirectUriTest extends AbstractKeycloakTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterAbstractKeycloakTest() {
|
public void afterAbstractKeycloakTest() throws Exception {
|
||||||
super.afterAbstractKeycloakTest();
|
super.afterAbstractKeycloakTest();
|
||||||
|
|
||||||
server.stop(0);
|
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.dom.saml.v2.protocol.ResponseType;
|
||||||
import org.keycloak.protocol.saml.SamlProtocol;
|
import org.keycloak.protocol.saml.SamlProtocol;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
|
||||||
import org.keycloak.services.resources.RealmsResource;
|
import org.keycloak.services.resources.RealmsResource;
|
||||||
import org.keycloak.testsuite.AbstractAuthTest;
|
import org.keycloak.testsuite.AbstractAuthTest;
|
||||||
import org.keycloak.testsuite.util.SamlClient;
|
import org.keycloak.testsuite.util.SamlClient;
|
||||||
|
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import javax.ws.rs.core.UriBuilderException;
|
import javax.ws.rs.core.UriBuilderException;
|
||||||
|
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.security.KeyFactory;
|
import java.security.KeyFactory;
|
||||||
import java.security.PrivateKey;
|
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_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_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_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_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 String SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVG8a7xGN6ZIkDbeecySygcDfsypjUMNPE4QJjis8B316CvsZQ0hcTTLUyiRpHlHZys2k3xEhHBHymFC1AONcvzZzpb40tAhLHO1qtAnut00khjAdjR3muLVdGkM/zMC7G5s9iIwBVhwOQhy+VsGnCH91EzkjZ4SVEr55KJoyQJQIDAQAB";
|
||||||
public static final PrivateKey SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY_PK;
|
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_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_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_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_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";
|
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": {
|
"connectionsHttpClient": {
|
||||||
"default": {
|
"default": {
|
||||||
"max-connection-idle-time-millis": 100
|
"reuse-connections": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -235,5 +235,9 @@
|
||||||
"dir": "target/dependency/vault",
|
"dir": "target/dependency/vault",
|
||||||
"enabled": "${keycloak.vault.files-plaintext.provider.enabled:false}"
|
"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) {
|
public void setEnabled(boolean enabled) {
|
||||||
enabledSwitch.setOn(enabled);
|
enabledSwitch.setOn(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAlwaysDisplayInConsole() {
|
public boolean isAlwaysDisplayInConsole() {
|
||||||
return alwaysDisplayInConsole.isOn();
|
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 ClientSettings clientSettingsPage;
|
||||||
|
|
||||||
private ClientRepresentation newClient;
|
private ClientRepresentation newClient;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void crudOIDCPublic() {
|
public void crudOIDCPublic() {
|
||||||
newClient = createClientRep("oidc-public", OIDC);
|
newClient = createClientRep("oidc-public", OIDC);
|
||||||
|
@ -60,22 +60,22 @@ public class ClientSettingsTest extends AbstractClientTest {
|
||||||
ClientRepresentation found = findClientByClientId(newClient.getClientId());
|
ClientRepresentation found = findClientByClientId(newClient.getClientId());
|
||||||
assertNotNull("Client " + newClient.getClientId() + " was not found.", found);
|
assertNotNull("Client " + newClient.getClientId() + " was not found.", found);
|
||||||
assertClientSettingsEqual(newClient, found);
|
assertClientSettingsEqual(newClient, found);
|
||||||
|
|
||||||
// update & verify
|
// update & verify
|
||||||
newClient.setClientId("oidc-public-updated");
|
newClient.setClientId("oidc-public-updated");
|
||||||
newClient.setName("updatedName");
|
newClient.setName("updatedName");
|
||||||
|
|
||||||
List<String> redirectUris = new ArrayList<>();
|
List<String> redirectUris = new ArrayList<>();
|
||||||
redirectUris.add("http://example2.test/app/*");
|
redirectUris.add("http://example2.test/app/*");
|
||||||
redirectUris.add("http://example2.test/app2/*");
|
redirectUris.add("http://example2.test/app2/*");
|
||||||
redirectUris.add("http://example3.test/app/*");
|
redirectUris.add("http://example3.test/app/*");
|
||||||
newClient.setRedirectUris(redirectUris);
|
newClient.setRedirectUris(redirectUris);
|
||||||
|
|
||||||
List<String> webOrigins = new ArrayList<>();
|
List<String> webOrigins = new ArrayList<>();
|
||||||
webOrigins.add("http://example2.test");
|
webOrigins.add("http://example2.test");
|
||||||
webOrigins.add("http://example3.test");
|
webOrigins.add("http://example3.test");
|
||||||
newClient.setWebOrigins(webOrigins);
|
newClient.setWebOrigins(webOrigins);
|
||||||
|
|
||||||
clientSettingsPage.form().setClientId("oidc-public-updated");
|
clientSettingsPage.form().setClientId("oidc-public-updated");
|
||||||
clientSettingsPage.form().setName("updatedName");
|
clientSettingsPage.form().setName("updatedName");
|
||||||
clientSettingsPage.form().setRedirectUris(redirectUris);
|
clientSettingsPage.form().setRedirectUris(redirectUris);
|
||||||
|
@ -84,7 +84,7 @@ public class ClientSettingsTest extends AbstractClientTest {
|
||||||
assertAlertSuccess();
|
assertAlertSuccess();
|
||||||
|
|
||||||
assertFalse(clientSettingsPage.form().isAlwaysDisplayInConsoleVisible());
|
assertFalse(clientSettingsPage.form().isAlwaysDisplayInConsoleVisible());
|
||||||
|
|
||||||
found = findClientByClientId(newClient.getClientId());
|
found = findClientByClientId(newClient.getClientId());
|
||||||
assertNotNull("Client " + newClient.getClientId() + " was not found.", found);
|
assertNotNull("Client " + newClient.getClientId() + " was not found.", found);
|
||||||
assertClientSettingsEqual(newClient, found);
|
assertClientSettingsEqual(newClient, found);
|
||||||
|
@ -130,10 +130,10 @@ public class ClientSettingsTest extends AbstractClientTest {
|
||||||
public void createOIDCConfidential() {
|
public void createOIDCConfidential() {
|
||||||
newClient = createClientRep("oidc-confidetial", OIDC);
|
newClient = createClientRep("oidc-confidetial", OIDC);
|
||||||
createClient(newClient);
|
createClient(newClient);
|
||||||
|
|
||||||
newClient.setRedirectUris(TEST_REDIRECT_URIs);
|
newClient.setRedirectUris(TEST_REDIRECT_URIs);
|
||||||
newClient.setPublicClient(false);
|
newClient.setPublicClient(false);
|
||||||
|
|
||||||
clientSettingsPage.form().setAccessType(CONFIDENTIAL);
|
clientSettingsPage.form().setAccessType(CONFIDENTIAL);
|
||||||
clientSettingsPage.form().setRedirectUris(TEST_REDIRECT_URIs);
|
clientSettingsPage.form().setRedirectUris(TEST_REDIRECT_URIs);
|
||||||
clientSettingsPage.form().save();
|
clientSettingsPage.form().save();
|
||||||
|
@ -142,29 +142,29 @@ public class ClientSettingsTest extends AbstractClientTest {
|
||||||
assertNotNull("Client " + newClient.getClientId() + " was not found.", found);
|
assertNotNull("Client " + newClient.getClientId() + " was not found.", found);
|
||||||
assertClientSettingsEqual(newClient, found);
|
assertClientSettingsEqual(newClient, found);
|
||||||
}
|
}
|
||||||
|
|
||||||
//KEYCLOAK-4022
|
//KEYCLOAK-4022
|
||||||
@Test
|
@Test
|
||||||
public void testOIDCConfidentialServiceAccountRolesTab() {
|
public void testOIDCConfidentialServiceAccountRolesTab() {
|
||||||
newClient = createClientRep("oidc-service-account-tab", OIDC);
|
newClient = createClientRep("oidc-service-account-tab", OIDC);
|
||||||
createClient(newClient);
|
createClient(newClient);
|
||||||
|
|
||||||
newClient.setRedirectUris(TEST_REDIRECT_URIs);
|
newClient.setRedirectUris(TEST_REDIRECT_URIs);
|
||||||
newClient.setPublicClient(false);
|
newClient.setPublicClient(false);
|
||||||
|
|
||||||
clientSettingsPage.form().setAccessType(CONFIDENTIAL);
|
clientSettingsPage.form().setAccessType(CONFIDENTIAL);
|
||||||
clientSettingsPage.form().setServiceAccountsEnabled(true);
|
clientSettingsPage.form().setServiceAccountsEnabled(true);
|
||||||
assertTrue(clientSettingsPage.form().isServiceAccountsEnabled());
|
assertTrue(clientSettingsPage.form().isServiceAccountsEnabled());
|
||||||
//check if Service Account Roles tab is not present
|
//check if Service Account Roles tab is not present
|
||||||
assertFalse(clientSettingsPage.tabs().isServiceAccountRolesDisplayed());
|
assertFalse(clientSettingsPage.tabs().isServiceAccountRolesDisplayed());
|
||||||
|
|
||||||
clientSettingsPage.form().setRedirectUris(TEST_REDIRECT_URIs);
|
clientSettingsPage.form().setRedirectUris(TEST_REDIRECT_URIs);
|
||||||
clientSettingsPage.form().save();
|
clientSettingsPage.form().save();
|
||||||
|
|
||||||
//should be there now
|
//should be there now
|
||||||
assertTrue(clientSettingsPage.tabs().getTabs().findElement(By.linkText("Service Account Roles")).isDisplayed());
|
assertTrue(clientSettingsPage.tabs().getTabs().findElement(By.linkText("Service Account Roles")).isDisplayed());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void saveOIDCConfidentialWithoutRedirectURIs() {
|
public void saveOIDCConfidentialWithoutRedirectURIs() {
|
||||||
newClient = createClientRep("oidc-confidential", OIDC);
|
newClient = createClientRep("oidc-confidential", OIDC);
|
||||||
|
@ -182,10 +182,10 @@ public class ClientSettingsTest extends AbstractClientTest {
|
||||||
|
|
||||||
clientSettingsPage.form().setAccessType(BEARER_ONLY);
|
clientSettingsPage.form().setAccessType(BEARER_ONLY);
|
||||||
clientSettingsPage.form().save();
|
clientSettingsPage.form().save();
|
||||||
|
|
||||||
newClient.setBearerOnly(true);
|
newClient.setBearerOnly(true);
|
||||||
newClient.setPublicClient(false);
|
newClient.setPublicClient(false);
|
||||||
|
|
||||||
ClientRepresentation found = findClientByClientId(newClient.getClientId());
|
ClientRepresentation found = findClientByClientId(newClient.getClientId());
|
||||||
assertNotNull("Client " + newClient.getClientId() + " was not found.", found);
|
assertNotNull("Client " + newClient.getClientId() + " was not found.", found);
|
||||||
assertClientSettingsEqual(newClient, found);
|
assertClientSettingsEqual(newClient, found);
|
||||||
|
@ -201,7 +201,7 @@ public class ClientSettingsTest extends AbstractClientTest {
|
||||||
assertClientSettingsEqual(newClient, found);
|
assertClientSettingsEqual(newClient, found);
|
||||||
assertClientSamlAttributes(getSAMLAttributes(), found.getAttributes());
|
assertClientSamlAttributes(getSAMLAttributes(), found.getAttributes());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void invalidSettings() {
|
public void invalidSettings() {
|
||||||
clientsPage.table().createClient();
|
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-authnstatement.tooltip=Should a statement specifying the method and timestamp be included in login responses?
|
||||||
include-onetimeuse-condition=Include OneTimeUse Condition
|
include-onetimeuse-condition=Include OneTimeUse Condition
|
||||||
include-onetimeuse-condition.tooltip=Should a OneTimeUse Condition be included in login responses?
|
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=Sign Documents
|
||||||
sign-documents.tooltip=Should SAML documents be signed by the realm?
|
sign-documents.tooltip=Should SAML documents be signed by the realm?
|
||||||
sign-documents-redirect-enable-key-info-ext=Optimize REDIRECT signing key lookup
|
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-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=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-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=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.
|
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
|
oidc-compatibility-modes=OpenID Connect Compatibility Modes
|
||||||
|
|
|
@ -1097,6 +1097,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
|
||||||
$scope.samlAuthnStatement = false;
|
$scope.samlAuthnStatement = false;
|
||||||
$scope.samlOneTimeUseCondition = false;
|
$scope.samlOneTimeUseCondition = false;
|
||||||
$scope.samlMultiValuedRoles = false;
|
$scope.samlMultiValuedRoles = false;
|
||||||
|
$scope.samlArtifactBinding = false;
|
||||||
$scope.samlServerSignature = false;
|
$scope.samlServerSignature = false;
|
||||||
$scope.samlServerSignatureEnableKeyInfoExtension = false;
|
$scope.samlServerSignatureEnableKeyInfoExtension = false;
|
||||||
$scope.samlAssertionSignature = 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') {
|
} else if ($scope.client.attributes['saml_name_id_format'] == 'persistent') {
|
||||||
$scope.nameIdFormat = $scope.nameIdFormats[3];
|
$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"]) {
|
||||||
if ($scope.client.attributes["saml.server.signature"] == "true") {
|
if ($scope.client.attributes["saml.server.signature"] == "true") {
|
||||||
$scope.samlServerSignature = true;
|
$scope.samlServerSignature = true;
|
||||||
|
@ -1635,6 +1646,11 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
|
||||||
}
|
}
|
||||||
delete $scope.clientEdit.requestUris;
|
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) {
|
if ($scope.samlServerSignature == true) {
|
||||||
$scope.clientEdit.attributes["saml.server.signature"] = "true";
|
$scope.clientEdit.attributes["saml.server.signature"] = "true";
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -171,6 +171,15 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'include-onetimeuse-condition.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'include-onetimeuse-condition.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</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'">
|
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
|
||||||
<label class="col-md-2 control-label" for="samlServerSignature">{{:: 'sign-documents' | translate}}</label>
|
<label class="col-md-2 control-label" for="samlServerSignature">{{:: 'sign-documents' | translate}}</label>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
|
@ -416,12 +425,35 @@
|
||||||
<kc-tooltip>{{:: 'logout-service-post-binding-url.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'logout-service-post-binding-url.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
|
<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">
|
<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" />
|
<input ng-model="clientEdit.attributes.saml_single_logout_service_url_redirect" class="form-control" type="text" name="logoutRedirectBinding" id="logoutRedirectBinding" />
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'logout-service-redir-binding-url.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'logout-service-redir-binding-url.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<fieldset data-ng-show="protocol == 'openid-connect'">
|
<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.title=Authentication Redirect
|
||||||
saml.post-form.message=Redirecting, please wait.
|
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.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
|
#authenticators
|
||||||
otp-display-name=Authenticator Application
|
otp-display-name=Authenticator Application
|
||||||
|
|
Loading…
Reference in a new issue