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:
AlistairDoswald 2019-08-13 17:45:35 +02:00 committed by Hynek Mlnařík
parent 64ccbda5d5
commit 8b3e77bf81
86 changed files with 4900 additions and 240 deletions

View file

@ -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() {
}
}

View file

@ -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";
}
}

View file

@ -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

View file

@ -98,6 +98,8 @@ public interface GeneralConstants {
String SAML_RESPONSE_KEY = "SAMLResponse";
String SAML_ARTIFACT_KEY = "SAMLart";
String SAML_SIG_ALG_REQUEST_KEY = "SigAlg";
String SAML_SIGNATURE_REQUEST_KEY = "Signature";

View file

@ -88,6 +88,7 @@ public enum JBossSAMLURIConstants {
SAML_HTTP_REDIRECT_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"),
SAML_SOAP_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:SOAP"),
SAML_PAOS_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:PAOS"),
SAML_HTTP_ARTIFACT_BINDING("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"),
SAML_11_NS("urn:oasis:names:tc:SAML:1.0:assertion"),

View file

@ -48,6 +48,8 @@ import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
import static org.keycloak.common.util.HtmlUtils.escapeAttribute;
import static org.keycloak.saml.common.util.StringUtil.isNotNull;
@ -311,7 +313,6 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
}
public String buildHtml(String samlResponse, String actionUrl, boolean asRequest) {
StringBuilder builder = new StringBuilder();
String key = GeneralConstants.SAML_RESPONSE_KEY;
@ -319,32 +320,44 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
key = GeneralConstants.SAML_REQUEST_KEY;
}
Map<String, String> inputTypes = new HashMap<>();
inputTypes.put(key, samlResponse);
if (isNotNull(relayState)) {
inputTypes.put(GeneralConstants.RELAY_STATE, relayState);
}
return buildHtmlForm(actionUrl, inputTypes);
}
public String buildHtmlForm(String actionUrl, Map<String, String> inputTypes) {
StringBuilder builder = new StringBuilder();
builder.append("<HTML>")
.append("<HEAD>")
.append("<HEAD>")
.append("<TITLE>Authentication Redirect</TITLE>")
.append("</HEAD>")
.append("<BODY Onload=\"document.forms[0].submit()\">")
.append("<TITLE>Authentication Redirect</TITLE>")
.append("</HEAD>")
.append("<BODY Onload=\"document.forms[0].submit()\">")
.append("<FORM METHOD=\"POST\" ACTION=\"").append(actionUrl).append("\">")
.append("<INPUT TYPE=\"HIDDEN\" NAME=\"").append(key).append("\"").append(" VALUE=\"").append(samlResponse).append("\"/>");
.append("<FORM METHOD=\"POST\" ACTION=\"").append(actionUrl).append("\">");
builder.append("<p>Redirecting, please wait.</p>");
if (isNotNull(relayState)) {
builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"RelayState\" " + "VALUE=\"").append(escapeAttribute(relayState)).append("\"/>");
for (String key: inputTypes.keySet()) {
builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"").append(key).append("\"").append(" VALUE=\"").append(escapeAttribute(inputTypes.get(key))).append("\"/>");
}
builder.append("<NOSCRIPT>")
.append("<P>JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue.</P>")
.append("<INPUT TYPE=\"SUBMIT\" VALUE=\"CONTINUE\" />")
.append("</NOSCRIPT>")
.append("<P>JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue.</P>")
.append("<INPUT TYPE=\"SUBMIT\" VALUE=\"CONTINUE\" />")
.append("</NOSCRIPT>")
.append("</FORM></BODY></HTML>");
.append("</FORM></BODY></HTML>");
return builder.toString();
}
public String base64Encoded(Document document) throws ConfigurationException, ProcessingException, IOException {
String documentAsString = DocumentUtil.getDocumentAsString(document);
logger.debugv("saml document: {0}", documentAsString);
@ -359,7 +372,7 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
int pos = builder.getQuery() == null? 0 : builder.getQuery().length();
builder.queryParam(samlParameterName, base64Encoded(document));
if (relayState != null) {
builder.queryParam("RelayState", relayState);
builder.queryParam(GeneralConstants.RELAY_STATE, relayState);
}
if (sign) {

View file

@ -51,11 +51,20 @@ import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_
public class SPMetadataDescriptor {
public static String getSPDescriptor(URI binding, URI assertionEndpoint, URI logoutEndpoint,
boolean wantAuthnRequestsSigned, boolean wantAssertionsSigned, boolean wantAssertionsEncrypted,
String entityId, String nameIDPolicyFormat, List<Element> signingCerts, List<Element> encryptionCerts)
throws XMLStreamException, ProcessingException, ParserConfigurationException
{
return getSPDescriptor(binding, binding, assertionEndpoint, logoutEndpoint, wantAuthnRequestsSigned,
wantAssertionsSigned, wantAssertionsEncrypted, entityId, nameIDPolicyFormat, signingCerts,
encryptionCerts);
}
public static String getSPDescriptor(URI loginBinding, URI logoutBinding, URI assertionEndpoint, URI logoutEndpoint,
boolean wantAuthnRequestsSigned, boolean wantAssertionsSigned, boolean wantAssertionsEncrypted,
String entityId, String nameIDPolicyFormat, List<Element> signingCerts, List<Element> encryptionCerts)
throws XMLStreamException, ProcessingException, ParserConfigurationException
{
StringWriter sw = new StringWriter();
XMLStreamWriter writer = StaxUtil.getXMLStreamWriter(sw);
SAMLMetadataWriter metadataWriter = new SAMLMetadataWriter(writer);
@ -67,7 +76,7 @@ public class SPMetadataDescriptor {
spSSODescriptor.setAuthnRequestsSigned(wantAuthnRequestsSigned);
spSSODescriptor.setWantAssertionsSigned(wantAssertionsSigned);
spSSODescriptor.addNameIDFormat(nameIDPolicyFormat);
spSSODescriptor.addSingleLogoutService(new EndpointType(binding, logoutEndpoint));
spSSODescriptor.addSingleLogoutService(new EndpointType(logoutBinding, logoutEndpoint));
if (wantAuthnRequestsSigned && signingCerts != null) {
for (Element key: signingCerts)
@ -89,7 +98,7 @@ public class SPMetadataDescriptor {
}
}
IndexedEndpointType assertionConsumerEndpoint = new IndexedEndpointType(binding, assertionEndpoint);
IndexedEndpointType assertionConsumerEndpoint = new IndexedEndpointType(loginBinding, assertionEndpoint);
assertionConsumerEndpoint.setIsDefault(true);
assertionConsumerEndpoint.setIndex(1);
spSSODescriptor.addAssertionConsumerService(assertionConsumerEndpoint);

View file

@ -177,7 +177,17 @@ public class SAML2Request {
throw logger.nullArgumentError("InputStream");
Document samlDocument = DocumentUtil.getDocument(is);
return getSAML2ObjectFromDocument(samlDocument);
}
/**
* Get the Underlying SAML2Object from a document
* @param samlDocument a Document containing a SAML2Object
* @return a SAMLDocumentHolder
* @throws ProcessingException
* @throws ParsingException
*/
public static SAMLDocumentHolder getSAML2ObjectFromDocument(Document samlDocument) throws ProcessingException, ParsingException {
SAMLParser samlParser = SAMLParser.getInstance();
JAXPValidationUtil.checkSchemaValidation(samlDocument);
SAML2Object requestType = (SAML2Object) samlParser.parse(samlDocument);

View file

@ -90,6 +90,16 @@ public class SAMLArtifactResponseParser extends SAMLStatusResponseTypeParser<Art
target.setStatus(SAMLStatusParser.getInstance().parse(xmlEventReader));
break;
case LOGOUT_REQUEST:
SAMLSloRequestParser sloRequestParser = SAMLSloRequestParser.getInstance();
target.setAny(sloRequestParser.parse(xmlEventReader));
break;
case LOGOUT_RESPONSE:
SAMLSloResponseParser sloResponseParser = SAMLSloResponseParser.getInstance();
target.setAny(sloResponseParser.parse(xmlEventReader));
break;
default:
throw LOGGER.parserUnknownTag(StaxParserUtil.getElementName(elementDetail), elementDetail.getLocation());
}

View file

@ -21,6 +21,7 @@ import org.keycloak.dom.saml.v2.assertion.EncryptedAssertionType;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
import org.keycloak.dom.saml.v2.protocol.StatusDetailType;
@ -40,6 +41,8 @@ import java.util.List;
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
import javax.xml.crypto.dsig.XMLSignature;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_NSURI;
/**
* Write a SAML Response to stream
*
@ -135,9 +138,16 @@ public class SAMLResponseWriter extends BaseWriter {
AuthnRequestType authn = (AuthnRequestType) anyObj;
SAMLRequestWriter requestWriter = new SAMLRequestWriter(writer);
requestWriter.write(authn);
} else if (anyObj instanceof LogoutRequestType) {
LogoutRequestType logoutRequestType = (LogoutRequestType) anyObj;
SAMLRequestWriter requestWriter = new SAMLRequestWriter(writer);
requestWriter.write(logoutRequestType);
} else if (anyObj instanceof ResponseType) {
ResponseType rt = (ResponseType) anyObj;
write(rt);
} else if (anyObj instanceof StatusResponseType) {
StatusResponseType rt = (StatusResponseType) anyObj;
write(rt, new QName(PROTOCOL_NSURI.get(), JBossSAMLConstants.LOGOUT_RESPONSE.get(), "samlp"));
}
StaxUtil.writeEndElement(writer);

View file

@ -52,6 +52,8 @@ public interface Errors {
String INVALID_SAML_AUTHN_REQUEST = "invalid_authn_request";
String INVALID_SAML_LOGOUT_REQUEST = "invalid_logout_request";
String INVALID_SAML_LOGOUT_RESPONSE = "invalid_logout_response";
String INVALID_SAML_ARTIFACT = "invalid_artifact";
String INVALID_SAML_ARTIFACT_RESPONSE = "invalid_artifact_response";
String SAML_TOKEN_NOT_FOUND = "saml_token_not_found";
String INVALID_SIGNATURE = "invalid_signature";
String INVALID_REGISTRATION = "invalid_registration";

View file

@ -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);
}

View file

@ -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> {
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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> {
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -29,6 +29,7 @@ org.keycloak.models.ActionTokenStoreSpi
org.keycloak.models.CodeToTokenStoreSpi
org.keycloak.models.OAuth2DeviceTokenStoreSpi
org.keycloak.models.OAuth2DeviceUserCodeSpi
org.keycloak.models.SamlArtifactSessionMappingStoreSpi
org.keycloak.models.SingleUseTokenStoreSpi
org.keycloak.models.TokenRevocationStoreSpi
org.keycloak.models.UserSessionSpi
@ -73,6 +74,7 @@ org.keycloak.authorization.store.StoreFactorySpi
org.keycloak.authorization.AuthorizationSpi
org.keycloak.models.cache.authorization.CachedStoreFactorySpi
org.keycloak.protocol.oidc.TokenIntrospectionSpi
org.keycloak.protocol.saml.ArtifactResolverSpi
org.keycloak.policy.PasswordPolicySpi
org.keycloak.policy.PasswordPolicyManagerSpi
org.keycloak.transaction.TransactionManagerLookupSpi

View file

@ -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;
}
}

View file

@ -88,7 +88,8 @@ public interface UserSessionModel {
enum State {
LOGGED_IN,
LOGGING_OUT,
LOGGED_OUT
LOGGED_OUT,
LOGGED_OUT_UNCONFIRMED;
}
/**

View file

@ -20,6 +20,7 @@ package org.keycloak.broker.saml;
import org.keycloak.broker.provider.DefaultDataMarshaller;
import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.exceptions.ParsingException;
@ -58,6 +59,10 @@ public class SAMLDataMarshaller extends DefaultDataMarshaller {
AuthnStatementType authnStatement = (AuthnStatementType) obj;
SAMLAssertionWriter samlWriter = new SAMLAssertionWriter(StaxUtil.getXMLStreamWriter(bos));
samlWriter.write(authnStatement, true);
} else if (obj instanceof ArtifactResponseType) {
ArtifactResponseType artifactResponseType = (ArtifactResponseType) obj;
SAMLResponseWriter samlWriter = new SAMLResponseWriter(StaxUtil.getXMLStreamWriter(bos));
samlWriter.write(artifactResponseType);
} else {
throw new IllegalArgumentException("Don't know how to serialize object of type " + obj.getClass().getName());
}
@ -77,7 +82,7 @@ public class SAMLDataMarshaller extends DefaultDataMarshaller {
String xmlString = serialized;
try {
if (clazz.equals(ResponseType.class) || clazz.equals(AssertionType.class) || clazz.equals(AuthnStatementType.class)) {
if (clazz.equals(ResponseType.class) || clazz.equals(AssertionType.class) || clazz.equals(AuthnStatementType.class) || clazz.equals(ArtifactResponseType.class)) {
byte[] bytes = xmlString.getBytes(GeneralConstants.SAML_CHARSET);
InputStream is = new ByteArrayInputStream(bytes);
Object respType = SAMLParser.getInstance().parse(is);

View file

@ -138,6 +138,7 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
int maxPooledPerRoute = config.getInt("max-pooled-per-route", 64);
int connectionPoolSize = config.getInt("connection-pool-size", 128);
long connectionTTL = config.getLong("connection-ttl-millis", -1L);
boolean reuseConnections = config.getBoolean("reuse-connections", true);
long maxConnectionIdleTime = config.getLong("max-connection-idle-time-millis", 900000L);
boolean disableCookies = config.getBoolean("disable-cookies", true);
String clientKeystore = config.get("client-keystore");
@ -152,6 +153,7 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
.establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS)
.maxPooledPerRoute(maxPooledPerRoute)
.connectionPoolSize(connectionPoolSize)
.reuseConnections(reuseConnections)
.connectionTTL(connectionTTL, TimeUnit.MILLISECONDS)
.maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS)
.disableCookies(disableCookies)

View file

@ -24,6 +24,7 @@ import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.conn.ssl.StrictHostnameVerifier;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.impl.NoConnectionReuseStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
@ -95,6 +96,7 @@ public class HttpClientBuilder {
protected int connectionPoolSize = 128;
protected int maxPooledPerRoute = 64;
protected long connectionTTL = -1;
protected boolean reuseConnections = true;
protected TimeUnit connectionTTLUnit = TimeUnit.MILLISECONDS;
protected long maxConnectionIdleTime = 900000;
protected TimeUnit maxConnectionIdleTimeUnit = TimeUnit.MILLISECONDS;
@ -140,6 +142,11 @@ public class HttpClientBuilder {
return this;
}
public HttpClientBuilder reuseConnections(boolean reuseConnections) {
this.reuseConnections = reuseConnections;
return this;
}
public HttpClientBuilder maxConnectionIdleTime(long maxConnectionIdleTime, TimeUnit unit) {
this.maxConnectionIdleTime = maxConnectionIdleTime;
this.maxConnectionIdleTimeUnit = unit;
@ -289,6 +296,9 @@ public class HttpClientBuilder {
.setMaxConnPerRoute(maxPooledPerRoute)
.setConnectionTimeToLive(connectionTTL, connectionTTLUnit);
if (!reuseConnections) {
builder.setConnectionReuseStrategy(new NoConnectionReuseStrategy());
}
if (proxyMappings != null && !proxyMappings.isEmpty()) {
builder.setRoutePlanner(new ProxyMappingsAwareRoutePlanner(proxyMappings));

View file

@ -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() {
}
}

View file

@ -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";
}
}

View file

@ -107,6 +107,46 @@ public class EntityDescriptorDescriptionConverter implements ClientDescriptionCo
return null;
}
/**
* Gets from a SPSSO descriptor the artifact resolution service for a given index
* @param sp an SPSSO descriptor
* @param index the index of the artifact resolution service to return
* @return the location of the artifact resolution service
*/
private static String getArtifactResolutionService(SPSSODescriptorType sp, int index) {
List<IndexedEndpointType> endpoints = sp.getArtifactResolutionService();
for (IndexedEndpointType endpoint : endpoints) {
if (endpoint.getIndex() == index) {
return endpoint.getLocation().toString();
}
}
return null;
}
/**
* Tries to get from a SPSSO descriptor the default artifact resolution service. Or if it doesn't
* exist, the artifact resolution service with the lowest index
* @param sp an SPSSO descriptor
* @return the location of the artifact resolution service
*/
private static String getArtifactResolutionService(SPSSODescriptorType sp) {
List<IndexedEndpointType> endpoints = sp.getArtifactResolutionService();
IndexedEndpointType firstEndpoint = null;
for (IndexedEndpointType endpoint : endpoints) {
if (endpoint.isIsDefault() != null && endpoint.isIsDefault()) {
firstEndpoint = endpoint;
break;
}
if (firstEndpoint == null || endpoint.getIndex() < firstEndpoint.getIndex()) {
firstEndpoint = endpoint;
}
}
if (firstEndpoint != null) {
return firstEndpoint.getLocation().toString();
}
return null;
}
private static ClientRepresentation loadEntityDescriptors(InputStream is) {
Object metadata;
try {
@ -172,6 +212,16 @@ public class EntityDescriptorDescriptionConverter implements ClientDescriptionCo
if (assertionConsumerServicePaosBinding != null) {
redirectUris.add(assertionConsumerServicePaosBinding);
}
String assertionConsumerServiceArtifactBinding = getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get());
if (assertionConsumerServiceArtifactBinding != null) {
attributes.put(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE, assertionConsumerServiceArtifactBinding);
redirectUris.add(assertionConsumerServiceArtifactBinding);
}
String artifactResolutionService = getArtifactResolutionService(spDescriptorType);
if (artifactResolutionService != null) {
attributes.put(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE, artifactResolutionService);
}
if (spDescriptorType.getNameIDFormat() != null) {
for (String format : spDescriptorType.getNameIDFormat()) {
String attribute = SamlClient.samlNameIDFormatToClientAttribute(format);

View file

@ -21,6 +21,7 @@ import org.keycloak.dom.saml.v2.metadata.EndpointType;
import org.keycloak.dom.saml.v2.metadata.EntitiesDescriptorType;
import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType;
import org.keycloak.dom.saml.v2.metadata.IDPSSODescriptorType;
import org.keycloak.dom.saml.v2.metadata.IndexedEndpointType;
import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType;
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
@ -38,6 +39,7 @@ import org.keycloak.saml.common.util.StaxUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.SAML_HTTP_POST_BINDING;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.SAML_SOAP_BINDING;
@ -54,7 +56,7 @@ import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_
public class IDPMetadataDescriptor {
public static String getIDPDescriptor(URI loginPostEndpoint, URI loginRedirectEndpoint, URI logoutEndpoint,
String entityId, boolean wantAuthnRequestsSigned, List<Element> signingCerts)
URI artifactResolutionService, String entityId, boolean wantAuthnRequestsSigned, List<Element> signingCerts)
throws ProcessingException
{
@ -76,9 +78,13 @@ public class IDPMetadataDescriptor {
spIDPDescriptor.addSingleLogoutService(new EndpointType(SAML_HTTP_POST_BINDING.getUri(), logoutEndpoint));
spIDPDescriptor.addSingleLogoutService(new EndpointType(SAML_HTTP_REDIRECT_BINDING.getUri(), logoutEndpoint));
spIDPDescriptor.addSingleLogoutService(new EndpointType(SAML_HTTP_ARTIFACT_BINDING.getUri(), logoutEndpoint));
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_HTTP_POST_BINDING.getUri(), loginPostEndpoint));
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_HTTP_REDIRECT_BINDING.getUri(), loginRedirectEndpoint));
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_SOAP_BINDING.getUri(), loginPostEndpoint));
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_HTTP_ARTIFACT_BINDING.getUri(), loginPostEndpoint));
spIDPDescriptor.addArtifactResolutionService(new IndexedEndpointType(SAML_SOAP_BINDING.getUri(), artifactResolutionService));
if (wantAuthnRequestsSigned && signingCerts != null) {
for (Element key: signingCerts)

View file

@ -119,6 +119,14 @@ public class SamlClient extends ClientConfigResolver {
client.setAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE, Boolean.toString(val));
}
public boolean forceArtifactBinding(){
return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING));
}
public void setForceArtifactBinding(boolean val) {
client.setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, Boolean.toString(val));
}
public boolean requiresRealmSignature() {
return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE));
}

View file

@ -31,6 +31,7 @@ public interface SamlConfigAttributes {
String SAML_AUTHNSTATEMENT = "saml.authnstatement";
String SAML_ONETIMEUSE_CONDITION = "saml.onetimeuse.condition";
String SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE = "saml_force_name_id_format";
String SAML_ARTIFACT_BINDING = "saml.artifact.binding";
String SAML_SERVER_SIGNATURE = "saml.server.signature";
String SAML_SERVER_SIGNATURE_KEYINFO_EXT = "saml.server.signature.keyinfo.ext";
String SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER = "saml.server.signature.keyinfo.xmlSigKeyInfoKeyNameTransformer";

View file

@ -23,11 +23,16 @@ import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.jboss.logging.Logger;
import org.keycloak.broker.saml.SAMLDataMarshaller;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
@ -38,6 +43,7 @@ import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SamlArtifactSessionMappingStoreProvider;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.LoginProtocol;
@ -51,6 +57,7 @@ import org.keycloak.saml.SAML2ErrorResponseBuilder;
import org.keycloak.saml.SAML2LoginResponseBuilder;
import org.keycloak.saml.SAML2LogoutRequestBuilder;
import org.keycloak.saml.SAML2LogoutResponseBuilder;
import org.keycloak.saml.SAML2NameIDBuilder;
import org.keycloak.saml.SamlProtocolExtensionsAwareBuilder.NodeGenerator;
import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.saml.common.constants.GeneralConstants;
@ -66,11 +73,12 @@ import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.sessions.CommonClientSessionModel;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.CommonClientSessionModel;
import org.w3c.dom.Document;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
@ -96,14 +104,15 @@ import org.apache.http.util.EntityUtils;
* @version $Revision: 1 $
*/
public class SamlProtocol implements LoginProtocol {
protected static final Logger logger = Logger.getLogger(SamlProtocol.class);
public static final String ATTRIBUTE_TRUE_VALUE = "true";
public static final String ATTRIBUTE_FALSE_VALUE = "false";
public static final String SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE = "saml_assertion_consumer_url_post";
public static final String SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE = "saml_assertion_consumer_url_redirect";
public static final String SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE = "saml_artifact_binding_url";
public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE = "saml_single_logout_service_url_post";
public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE = "saml_single_logout_service_url_artifact";
public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE = "saml_single_logout_service_url_redirect";
public static final String SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE = "saml_artifact_resolution_service_url";
public static final String LOGIN_PROTOCOL = "saml";
public static final String SAML_BINDING = "saml_binding";
public static final String SAML_IDP_INITIATED_LOGIN = "saml_idp_initiated_login";
@ -127,6 +136,9 @@ public class SamlProtocol implements LoginProtocol {
public static final String SAML_IDP_INITIATED_SSO_URL_NAME = "saml_idp_initiated_sso_url_name";
public static final String SAML_LOGIN_REQUEST_FORCEAUTHN = "SAML_LOGIN_REQUEST_FORCEAUTHN";
public static final String SAML_FORCEAUTHN_REQUIREMENT = "true";
public static final String SAML_LOGOUT_INITIATOR_CLIENT_ID = "SAML_LOGOUT_INITIATOR_CLIENT_ID";
protected static final Logger logger = Logger.getLogger(SamlProtocol.class);
protected KeycloakSession session;
@ -138,6 +150,9 @@ public class SamlProtocol implements LoginProtocol {
protected EventBuilder event;
protected ArtifactResolver artifactResolver;
protected SamlArtifactSessionMappingStoreProvider artifactSessionMappingStore;
@Override
public SamlProtocol setSession(KeycloakSession session) {
this.session = session;
@ -168,6 +183,20 @@ public class SamlProtocol implements LoginProtocol {
return this;
}
private ArtifactResolver getArtifactResolver() {
if (artifactResolver == null) {
artifactResolver = session.getProvider(ArtifactResolver.class);
}
return artifactResolver;
}
private SamlArtifactSessionMappingStoreProvider getArtifactSessionMappingStore() {
if (artifactSessionMappingStore == null) {
artifactSessionMappingStore = session.getProvider(SamlArtifactSessionMappingStoreProvider.class);
}
return artifactSessionMappingStore;
}
@Override
public Response sendError(AuthenticationSessionModel authSession, Error error) {
try {
@ -187,8 +216,8 @@ public class SamlProtocol implements LoginProtocol {
}
} else {
return samlErrorMessage(
authSession, new SamlClient(client), isPostBinding(authSession),
authSession.getRedirectUri(), translateErrorToSAMLStatus(error), authSession.getClientNote(GeneralConstants.RELAY_STATE)
authSession, new SamlClient(client), isPostBinding(authSession),
authSession.getRedirectUri(), translateErrorToSAMLStatus(error), authSession.getClientNote(GeneralConstants.RELAY_STATE)
);
}
} finally {
@ -197,8 +226,8 @@ public class SamlProtocol implements LoginProtocol {
}
private Response samlErrorMessage(
AuthenticationSessionModel authSession, SamlClient samlClient, boolean isPostBinding,
String destination, JBossSAMLURIConstants statusDetail, String relayState) {
AuthenticationSessionModel authSession, SamlClient samlClient, boolean isPostBinding,
String destination, JBossSAMLURIConstants statusDetail, String relayState) {
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(session).relayState(relayState);
SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(destination).issuer(getResponseIssuer(realm)).status(statusDetail.get());
KeyManager keyManager = session.keys();
@ -312,13 +341,13 @@ public class SamlProtocol implements LoginProtocol {
String configuredNameIdFormat = samlClient.getNameIDFormat();
if ((nameIdFormat == null || forceFormat) && configuredNameIdFormat != null) {
nameIdFormat = configuredNameIdFormat;
}
}
if (nameIdFormat == null)
return SAML_DEFAULT_NAMEID_FORMAT;
return nameIdFormat;
}
protected String getNameId(String nameIdFormat, CommonClientSessionModel clientSession, UserSessionModel userSession) {
protected String getNameId(String nameIdFormat, CommonClientSessionModel clientSession, UserSessionModel userSession) {
if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) {
final String email = userSession.getUser().getEmail();
if (email == null) {
@ -342,11 +371,11 @@ public class SamlProtocol implements LoginProtocol {
* Attempts to retrieve the persistent type NameId as follows:
*
* <ol>
* <li>saml.persistent.name.id.for.$clientId user attribute</li>
* <li>saml.persistent.name.id.for.* user attribute</li>
* <li>G-$randomUuid</li>
* <li>saml.persistent.name.id.for.$clientId user attribute</li>
* <li>saml.persistent.name.id.for.* user attribute</li>
* <li>G-$randomUuid</li>
* </ol>
*
* <p>
* If a randomUuid is generated, an attribute for the given saml.persistent.name.id.for.$clientId will be generated,
* otherwise no state change will occur with respect to the user's attributes.
*
@ -389,8 +418,8 @@ public class SamlProtocol implements LoginProtocol {
if (nameId == null) {
return samlErrorMessage(
null, samlClient, isPostBinding(authSession),
redirectUri, JBossSAMLURIConstants.STATUS_INVALID_NAMEIDPOLICY, relayState
null, samlClient, isPostBinding(authSession),
redirectUri, JBossSAMLURIConstants.STATUS_INVALID_NAMEIDPOLICY, relayState
);
}
@ -418,7 +447,7 @@ public class SamlProtocol implements LoginProtocol {
builder.disableAuthnStatement(true);
}
builder.includeOneTimeUseCondition(samlClient.includeOneTimeUseCondition());
builder.includeOneTimeUseCondition(samlClient.includeOneTimeUseCondition());
List<ProtocolMapperProcessor<SAMLAttributeStatementMapper>> attributeStatementMappers = new LinkedList<>();
List<ProtocolMapperProcessor<SAMLLoginResponseMapper>> loginResponseMappers = new LinkedList<>();
@ -441,17 +470,18 @@ public class SamlProtocol implements LoginProtocol {
});
Document samlDocument = null;
ResponseType samlModel = null;
KeyManager keyManager = session.keys();
KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm);
boolean postBinding = isPostBinding(authSession);
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
try {
if ((! postBinding) && samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
if ((!postBinding) && samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
builder.addExtension(new KeycloakKeySamlExtensionGenerator(keyName));
}
ResponseType samlModel = builder.buildModel();
samlModel = builder.buildModel();
final AttributeStatementType attributeStatement = populateAttributeStatements(attributeStatementMappers, session, userSession, clientSession);
populateRoles(roleListMapper.get(), session, userSession, clientSessionCtx, attributeStatement);
@ -462,7 +492,6 @@ public class SamlProtocol implements LoginProtocol {
}
samlModel = transformLoginResponse(loginResponseMappers, samlModel, session, userSession, clientSessionCtx);
samlDocument = builder.buildDocument(samlModel);
} catch (Exception e) {
logger.error("failed", e);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.FAILED_TO_PROCESS_RESPONSE);
@ -471,20 +500,26 @@ public class SamlProtocol implements LoginProtocol {
JaxrsSAML2BindingBuilder bindingBuilder = new JaxrsSAML2BindingBuilder(session);
bindingBuilder.relayState(relayState);
if (samlClient.requiresRealmSignature()) {
if ("true".equals(clientSession.getNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get()))) {
try {
return buildArtifactAuthenticatedResponse(clientSession, redirectUri, samlModel, bindingBuilder);
} catch (Exception e) {
logger.error("failed", e);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.FAILED_TO_PROCESS_RESPONSE);
}
}
if (samlClient.requiresRealmSignature() || samlClient.requiresAssertionSignature()) {
String canonicalization = samlClient.getCanonicalizationMethod();
if (canonicalization != null) {
bindingBuilder.canonicalizationMethod(canonicalization);
}
bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
}
if (samlClient.requiresAssertionSignature()) {
String canonicalization = samlClient.getCanonicalizationMethod();
if (canonicalization != null) {
bindingBuilder.canonicalizationMethod(canonicalization);
}
bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signAssertions();
bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate());
if (samlClient.requiresRealmSignature()) bindingBuilder.signDocument();
if (samlClient.requiresAssertionSignature()) bindingBuilder.signAssertions();
}
if (samlClient.requiresEncryption()) {
PublicKey publicKey = null;
try {
@ -496,6 +531,7 @@ public class SamlProtocol implements LoginProtocol {
bindingBuilder.encrypt(publicKey);
}
try {
samlDocument = builder.buildDocument(samlModel);
return buildAuthenticatedResponse(clientSession, redirectUri, samlDocument, bindingBuilder);
} catch (Exception e) {
logger.error("failed", e);
@ -551,19 +587,27 @@ public class SamlProtocol implements LoginProtocol {
roleListMapper.mapper.mapRoles(existingAttributeStatement, roleListMapper.model, session, userSession, clientSessionCtx);
}
public static String getLogoutServiceUrl(KeycloakSession session, ClientModel client, String bindingType) {
public static String getLogoutServiceUrl(KeycloakSession session, ClientModel client, String bindingType, boolean backChannelLogout) {
String logoutServiceUrl = null;
if (SAML_POST_BINDING.equals(bindingType)) {
// backchannel logout doesn't support sending artifacts
if (!backChannelLogout && useArtifactForLogout(client)) {
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE);
} else if (SAML_POST_BINDING.equals(bindingType)) {
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE);
} else {
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE);
}
if (logoutServiceUrl == null)
logoutServiceUrl = client.getManagementUrl();
if (logoutServiceUrl == null || logoutServiceUrl.trim().equals(""))
return null;
return ResourceAdminManager.resolveUri(session, client.getRootUrl(), logoutServiceUrl);
}
public static boolean useArtifactForLogout(ClientModel client) {
return new SamlClient(client).forceArtifactBinding()
&& client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE) != null;
}
@Override
@ -572,41 +616,41 @@ public class SamlProtocol implements LoginProtocol {
SamlClient samlClient = new SamlClient(client);
try {
boolean postBinding = isLogoutPostBindingForClient(clientSession);
String bindingUri = getLogoutServiceUrl(session, client, postBinding ? SAML_POST_BINDING : SAML_REDIRECT_BINDING);
String bindingUri = getLogoutServiceUrl(session, client, postBinding ? SAML_POST_BINDING : SAML_REDIRECT_BINDING, false);
if (bindingUri == null) {
logger.warnf("Failed to logout client %s, skipping this client. Please configure the logout service url in the admin console for your client applications.", client.getClientId());
return null;
}
if (postBinding) {
LogoutRequestType logoutRequest = createLogoutRequest(bindingUri, clientSession, client);
// This is POST binding, hence KeyID is included in dsig:KeyInfo/dsig:KeyName, no need to add <samlp:Extensions> element
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
return binding.postBinding(SAML2Request.convert(logoutRequest)).request(bindingUri);
} else {
logger.debug("frontchannel redirect binding");
NodeGenerator[] extensions;
NodeGenerator[] extensions = new NodeGenerator[]{};
if (!postBinding) {
if (samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
extensions = new NodeGenerator[] { new KeycloakKeySamlExtensionGenerator(keyName) };
} else {
extensions = new NodeGenerator[] {};
extensions = new NodeGenerator[]{new KeycloakKeySamlExtensionGenerator(keyName)};
}
LogoutRequestType logoutRequest = createLogoutRequest(bindingUri, clientSession, client, extensions);
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
return binding.redirectBinding(SAML2Request.convert(logoutRequest)).request(bindingUri);
}
} catch (ConfigurationException e) {
throw new RuntimeException(e);
} catch (ProcessingException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ParsingException e) {
LogoutRequestType logoutRequest = createLogoutRequest(bindingUri, clientSession, client, extensions);
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient, "true".equals(clientSession.getNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get())));
//If this session uses artifact binding, send an artifact instead of the LogoutRequest
if ("true".equals(clientSession.getNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get()))
&& useArtifactForLogout(client)) {
clientSession.setAction(CommonClientSessionModel.Action.LOGGING_OUT.name());
return buildArtifactAuthenticatedResponse(clientSession, bindingUri, logoutRequest, binding);
}
Document samlDocument = SAML2Request.convert(logoutRequest);
if (postBinding) {
// This is POST binding, hence KeyID is included in dsig:KeyInfo/dsig:KeyName, no need to add <samlp:Extensions> element
return binding.postBinding(samlDocument).request(bindingUri);
} else {
logger.debug("frontchannel redirect binding");
return binding.redirectBinding(samlDocument).request(bindingUri);
}
} catch (ConfigurationException | ProcessingException | IOException | ParsingException e) {
throw new RuntimeException(e);
}
}
@Override
@ -635,11 +679,11 @@ public class SamlProtocol implements LoginProtocol {
}
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
XmlKeyInfoKeyNameTransformer transformer = XmlKeyInfoKeyNameTransformer.from(
userSession.getNote(SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER),
SamlClient.DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER);
userSession.getNote(SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER),
SamlClient.DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER);
String keyName = transformer.getKeyName(keys.getKid(), keys.getCertificate());
binding.signatureAlgorithm(algorithm).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
boolean addExtension = (! postBinding) && Objects.equals("true", userSession.getNote(SamlProtocol.SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO));
boolean addExtension = (!postBinding) && Objects.equals("true", userSession.getNote(SamlProtocol.SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO));
if (addExtension) { // Only include extension if REDIRECT binding and signing whole SAML protocol message
builder.addExtension(new KeycloakKeySamlExtensionGenerator(keyName));
}
@ -647,7 +691,7 @@ public class SamlProtocol implements LoginProtocol {
Response response;
try {
response = buildLogoutResponse(userSession, logoutBindingUri, builder, binding);
} catch (ConfigurationException | ProcessingException | IOException e) {
} catch (ConfigurationException | ProcessingException | IOException e) {
throw new RuntimeException(e);
}
if (logoutBindingUri != null) {
@ -666,10 +710,18 @@ public class SamlProtocol implements LoginProtocol {
}
protected Response buildLogoutResponse(UserSessionModel userSession, String logoutBindingUri, SAML2LogoutResponseBuilder builder, JaxrsSAML2BindingBuilder binding) throws ConfigurationException, ProcessingException, IOException {
//if artifact binding is used, send an artifact instead of the LogoutResponse
if ("true".equals(userSession.getNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get()))) {
return buildLogoutArtifactResponse(userSession, logoutBindingUri, builder.buildModel(), binding);
}
Document samlDocument = builder.buildDocument();
if (isLogoutPostBindingForInitiator(userSession)) {
return binding.postBinding(builder.buildDocument()).response(logoutBindingUri);
return binding.postBinding(samlDocument).response(logoutBindingUri);
} else {
return binding.redirectBinding(builder.buildDocument()).response(logoutBindingUri);
return binding.redirectBinding(samlDocument).response(logoutBindingUri);
}
}
@ -677,7 +729,7 @@ public class SamlProtocol implements LoginProtocol {
public Response backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
ClientModel client = clientSession.getClient();
SamlClient samlClient = new SamlClient(client);
String logoutUrl = getLogoutServiceUrl(session, client, SAML_POST_BINDING);
String logoutUrl = getLogoutServiceUrl(session, client, SAML_POST_BINDING, true);
if (logoutUrl == null) {
logger.warnf("Can't do backchannel logout. No SingleLogoutService POST Binding registered for client: %s",
client.getClientId());
@ -687,7 +739,7 @@ public class SamlProtocol implements LoginProtocol {
String logoutRequestString = null;
try {
LogoutRequestType logoutRequest = createLogoutRequest(logoutUrl, clientSession, client);
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient, false);
// This is POST binding, hence KeyID is included in dsig:KeyInfo/dsig:KeyName, no need to add <samlp:Extensions> element
logoutRequestString = binding.postBinding(SAML2Request.convert(logoutRequest)).encoded();
} catch (Exception e) {
@ -701,8 +753,8 @@ public class SamlProtocol implements LoginProtocol {
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
formparams.add(new BasicNameValuePair(GeneralConstants.SAML_REQUEST_KEY, logoutRequestString));
formparams.add(new BasicNameValuePair("BACK_CHANNEL_LOGOUT", "BACK_CHANNEL_LOGOUT")); // for Picketlink
// todo remove
// this
// todo remove
// this
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
HttpPost post = new HttpPost(logoutUrl);
post.setEntity(form);
@ -742,10 +794,10 @@ public class SamlProtocol implements LoginProtocol {
logoutBuilder.addExtension(extension);
}
LogoutRequestType logoutRequest = logoutBuilder.createLogoutRequest();
for (Iterator<SamlAuthenticationPreprocessor> it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext();) {
for (Iterator<SamlAuthenticationPreprocessor> it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext(); ) {
logoutRequest = it.next().beforeSendingLogoutRequest(logoutRequest, clientSession.getUserSession(), clientSession);
}
return logoutRequest;
}
@ -755,9 +807,9 @@ public class SamlProtocol implements LoginProtocol {
return Objects.equals(SamlProtocol.SAML_FORCEAUTHN_REQUIREMENT, requireReauthentication);
}
private JaxrsSAML2BindingBuilder createBindingBuilder(SamlClient samlClient) {
private JaxrsSAML2BindingBuilder createBindingBuilder(SamlClient samlClient, boolean skipRealmSignature) {
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(session);
if (samlClient.requiresRealmSignature()) {
if (!skipRealmSignature && samlClient.requiresRealmSignature()) {
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
binding.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
@ -769,4 +821,145 @@ public class SamlProtocol implements LoginProtocol {
public void close() {
}
/**
* This method, instead of sending the actual response with the token sends
* the artifact message via post or redirect.
*
* @param clientSession the current authenticated client session
* @param redirectUri the redirect uri to the client
* @param samlDocument a Document containing the saml Response
* @param bindingBuilder the current JaxrsSAML2BindingBuilder configured with information for signing and encryption
* @return A response (POSTed form or redirect) with a newly generated artifact
* @throws ConfigurationException
* @throws ProcessingException
* @throws IOException
*/
protected Response buildArtifactAuthenticatedResponse(AuthenticatedClientSessionModel clientSession,
String redirectUri, SAML2Object samlDocument,
JaxrsSAML2BindingBuilder bindingBuilder)
throws ProcessingException, ConfigurationException {
try {
String artifact = buildArtifactAndStoreResponse(samlDocument, clientSession);
String relayState = clientSession.getNote(GeneralConstants.RELAY_STATE);
logger.debugf("Sending artifact %s to client %s", artifact, clientSession.getClient().getClientId());
if (isPostBinding(clientSession)) {
return artifactPost(redirectUri, artifact, relayState, bindingBuilder);
} else {
return artifactRedirect(redirectUri, artifact, relayState);
}
} catch (ArtifactResolverProcessingException e) {
throw new ProcessingException(e);
}
}
/**
* This method, instead of sending the actual response with the token, sends
* the artifact message via post or redirect. This method is only to be used for the final LogoutResponse.
*
* @param userSession The current user session being logged out
* @param redirectUri the redirect uri to the client
* @param statusResponseType a Document containing the saml Response
* @param bindingBuilder the current JaxrsSAML2BindingBuilder configured with information for signing and encryption
* @return A response (POSTed form or redirect) with a newly generated artifact
* @throws ProcessingException
* @throws IOException
*/
protected Response buildLogoutArtifactResponse(UserSessionModel userSession,
String redirectUri, StatusResponseType statusResponseType,
JaxrsSAML2BindingBuilder bindingBuilder)
throws ProcessingException, ConfigurationException {
try {
String artifact = buildArtifactAndStoreResponse(statusResponseType, userSession);
String relayState = userSession.getNote(SAML_LOGOUT_RELAY_STATE);
logger.debugf("Sending artifact for LogoutResponse %s to user %s", artifact, userSession.getLoginUsername());
if (isLogoutPostBindingForInitiator(userSession)) {
return artifactPost(redirectUri, artifact, relayState, bindingBuilder);
} else {
return artifactRedirect(redirectUri, artifact, relayState);
}
} catch (ArtifactResolverProcessingException e) {
throw new ProcessingException(e);
}
}
protected String buildArtifactAndStoreResponse(SAML2Object statusResponseType, UserSessionModel userSession) throws ArtifactResolverProcessingException, ConfigurationException, ProcessingException {
String clientIdThatInitiatedLogout = userSession.getNote(SAML_LOGOUT_INITIATOR_CLIENT_ID);
userSession.removeNote(SAML_LOGOUT_INITIATOR_CLIENT_ID);
AuthenticatedClientSessionModel clientSessionModel = userSession.getAuthenticatedClientSessionByClient(clientIdThatInitiatedLogout);
if (clientSessionModel == null) {
throw new IllegalStateException("Initiator client id is unknown when artifact response is created");
}
return buildArtifactAndStoreResponse(statusResponseType, clientSessionModel);
}
protected String buildArtifactAndStoreResponse(SAML2Object saml2Object, AuthenticatedClientSessionModel clientSessionModel) throws ArtifactResolverProcessingException, ProcessingException, ConfigurationException {
String entityId = RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString();
ArtifactResponseType artifactResponseType = SamlProtocolUtils.buildArtifactResponse(saml2Object, SAML2NameIDBuilder.value(getResponseIssuer(realm)).build());
// Create artifact and store session mapping
SAMLDataMarshaller marshaller = new SAMLDataMarshaller();
String artifact = getArtifactResolver().buildArtifact(clientSessionModel, entityId, marshaller.serialize(artifactResponseType));
getArtifactSessionMappingStore().put(artifact, realm.getAccessCodeLifespan(), clientSessionModel);
return artifact;
}
/**
* Return an artifact through a redirect message
*
* @param redirectUri the redirect uri to the client
* @param artifact the artifact to send
* @param relayState the current relayState
* @return a redirect Response with the artifact
*/
private Response artifactRedirect(String redirectUri, String artifact, String relayState) {
KeycloakUriBuilder builder = KeycloakUriBuilder.fromUri(redirectUri)
.replaceQuery(null)
.queryParam(GeneralConstants.SAML_ARTIFACT_KEY, artifact);
if (relayState != null) {
builder.queryParam(GeneralConstants.RELAY_STATE, relayState);
}
URI uri = builder.build();
return Response.status(302).location(uri)
.header("Pragma", "no-cache")
.header("Cache-Control", "no-cache, no-store").build();
}
/**
* Return an artifact through a POSTed form
*
* @param redirectUri the redirect uri to the client
* @param artifact the artifact to send
* @param relayState current relayState
* @param bindingBuilder the current JaxrsSAML2BindingBuilder configured with information for signing and encryption
* @return a POSTed form response, with the artifact
*/
private Response artifactPost(String redirectUri, String artifact, String relayState, JaxrsSAML2BindingBuilder bindingBuilder) {
Map<String, String> inputTypes = new HashMap<>();
inputTypes.put(GeneralConstants.SAML_ARTIFACT_KEY, artifact);
if (relayState != null) {
inputTypes.put(GeneralConstants.RELAY_STATE, relayState);
}
String str = bindingBuilder.buildHtmlForm(redirectUri, inputTypes);
return Response.ok(str, MediaType.TEXT_HTML_TYPE)
.header("Pragma", "no-cache")
.header("Cache-Control", "no-cache, no-store").build();
}
}

View file

@ -17,21 +17,37 @@
package org.keycloak.protocol.saml;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.security.Key;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.PemUtils;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
import org.keycloak.dom.saml.v2.protocol.StatusType;
import org.keycloak.models.ClientModel;
import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.common.util.StaxUtil;
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLResponseWriter;
import org.keycloak.saml.processing.web.util.RedirectBindingUtil;
import org.w3c.dom.Document;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriInfo;
import java.security.PublicKey;
import java.security.Signature;
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
@ -40,8 +56,12 @@ import org.keycloak.rotation.HardcodedKeyLocator;
import org.keycloak.rotation.KeyLocator;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import java.security.PublicKey;
import java.security.Signature;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import org.w3c.dom.Element;
/**
@ -198,4 +218,77 @@ public class SamlProtocolUtils {
return null;
}
/**
* Takes a saml object (an object that will be part of resulting ArtifactResponse), and inserts it as the body of
* an ArtifactResponse. The ArtifactResponse is returned as ArtifactResponseType
*
* @param samlObject a Saml object
* @param issuer issuer of the resulting ArtifactResponse, should be the same as issuer of the samlObject
* @param statusCode status code of the resulting response
* @return An ArtifactResponse containing the saml object.
*/
public static ArtifactResponseType buildArtifactResponse(SAML2Object samlObject, NameIDType issuer, URI statusCode) throws ConfigurationException, ProcessingException {
ArtifactResponseType artifactResponse = new ArtifactResponseType(IDGenerator.create("ID_"),
XMLTimeUtil.getIssueInstant());
// Status
StatusType statusType = new StatusType();
StatusCodeType statusCodeType = new StatusCodeType();
statusCodeType.setValue(statusCode);
statusType.setStatusCode(statusCodeType);
artifactResponse.setStatus(statusType);
artifactResponse.setIssuer(issuer);
artifactResponse.setAny(samlObject);
return artifactResponse;
}
/**
* Takes a saml object (an object that will be part of resulting ArtifactResponse), and inserts it as the body of
* an ArtifactResponse. The ArtifactResponse is returned as ArtifactResponseType
*
* @param samlObject a Saml object
* @param issuer issuer of the resulting ArtifactResponse, should be the same as issuer of the samlObject
* @return An ArtifactResponse containing the saml object.
*/
public static ArtifactResponseType buildArtifactResponse(SAML2Object samlObject, NameIDType issuer) throws ConfigurationException, ProcessingException {
return buildArtifactResponse(samlObject, issuer, JBossSAMLURIConstants.STATUS_SUCCESS.getUri());
}
/**
* Takes a saml document and inserts it as a body of ArtifactResponseType
* @param document the document
* @return An ArtifactResponse containing the saml document.
*/
public static ArtifactResponseType buildArtifactResponse(Document document) throws ParsingException, ProcessingException, ConfigurationException {
SAML2Object samlObject = SAML2Request.getSAML2ObjectFromDocument(document).getSamlObject();
if (samlObject instanceof StatusResponseType) {
return buildArtifactResponse(samlObject, ((StatusResponseType)samlObject).getIssuer());
} else if (samlObject instanceof RequestAbstractType) {
return buildArtifactResponse(samlObject, ((RequestAbstractType)samlObject).getIssuer());
}
throw new ProcessingException("SAMLObject was not StatusResponseType or LogoutRequestType");
}
/**
* Convert a SAML2 ArtifactResponse into a Document
* @param responseType an artifactResponse
*
* @return an artifact response converted to a Document
*
* @throws ParsingException
* @throws ConfigurationException
* @throws ProcessingException
*/
public static Document convert(ArtifactResponseType responseType) throws ProcessingException, ConfigurationException,
ParsingException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
SAMLResponseWriter writer = new SAMLResponseWriter(StaxUtil.getXMLStreamWriter(bos));
writer.write(responseType);
return DocumentUtil.getDocument(new ByteArrayInputStream(bos.toByteArray()));
}
}

View file

@ -61,6 +61,11 @@ public class SamlRepresentationAttributes {
return getAttributes().get(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE);
}
public String getSamlArtifactBinding() {
if (getAttributes() == null) return null;
return getAttributes().get(SamlConfigAttributes.SAML_ARTIFACT_BINDING);
}
public String getSamlServerSignature() {
if (getAttributes() == null) return null;
return getAttributes().get(SamlConfigAttributes.SAML_SERVER_SIGNATURE);

View file

@ -17,85 +17,134 @@
package org.keycloak.protocol.saml;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.specimpl.ResteasyHttpHeaders;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.broker.saml.SAMLDataMarshaller;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.PemUtils;
import org.keycloak.common.util.Resteasy;
import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyStatus;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.assertion.BaseIDAbstractType;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.assertion.SubjectType;
import org.keycloak.dom.saml.v2.protocol.ArtifactResolveType;
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.NameIDPolicyType;
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.executors.ExecutorsProvider;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.KeycloakUriInfo;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SamlArtifactSessionMappingModel;
import org.keycloak.models.SamlArtifactSessionMappingStoreProvider;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor;
import org.keycloak.protocol.saml.profile.ecp.SamlEcpProfileService;
import org.keycloak.protocol.saml.profile.util.Soap;
import org.keycloak.protocol.util.ArtifactBindingUtils;
import org.keycloak.rotation.HardcodedKeyLocator;
import org.keycloak.rotation.KeyLocator;
import org.keycloak.saml.BaseSAML2BindingBuilder;
import org.keycloak.saml.SAML2LogoutResponseBuilder;
import org.keycloak.saml.SAML2NameIDBuilder;
import org.keycloak.saml.SAMLRequestParser;
import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.common.util.StaxUtil;
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLRequestWriter;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.saml.processing.web.util.PostBindingUtil;
import org.keycloak.saml.processing.web.util.RedirectBindingUtil;
import org.keycloak.saml.validators.DestinationValidator;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.scheduled.ScheduledTaskRunner;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.CommonClientSessionModel;
import org.keycloak.timer.ScheduledTask;
import org.keycloak.transaction.AsyncResponseTransaction;
import org.keycloak.utils.MediaType;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.Suspended;
import javax.ws.rs.core.*;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.stream.XMLStreamWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.PublicKey;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.rotation.HardcodedKeyLocator;
import org.keycloak.rotation.KeyLocator;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.saml.validators.DestinationValidator;
import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.MultivaluedMap;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import static org.keycloak.common.util.StackUtil.getShortStackTrace;
/**
* Resource class for the saml connect token service
@ -106,6 +155,7 @@ import org.w3c.dom.NodeList;
public class SamlService extends AuthorizationEndpointBase {
protected static final Logger logger = Logger.getLogger(SamlService.class);
public static final String ARTIFACT_RESOLUTION_SERVICE_PATH = "resolve";
private final DestinationValidator destinationValidator;
@ -121,7 +171,8 @@ public class SamlService extends AuthorizationEndpointBase {
// and we want to turn it off.
protected boolean redirectToAuthentication;
protected Response basicChecks(String samlRequest, String samlResponse) {
protected Response basicChecks(String samlRequest, String samlResponse, String artifact) {
logger.tracef("basicChecks(%s, %s, %s)%s", samlRequest, samlResponse, artifact, getShortStackTrace());
if (!checkSsl()) {
event.event(EventType.LOGIN);
event.error(Errors.SSL_REQUIRED);
@ -133,7 +184,7 @@ public class SamlService extends AuthorizationEndpointBase {
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.REALM_NOT_ENABLED);
}
if (samlRequest == null && samlResponse == null) {
if (samlRequest == null && samlResponse == null && artifact == null) {
event.event(EventType.LOGIN);
event.error(Errors.SAML_TOKEN_NOT_FOUND);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
@ -216,6 +267,7 @@ public class SamlService extends AuthorizationEndpointBase {
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
}
SAML2Object samlObject = documentHolder.getSamlObject();
if (samlObject instanceof AuthnRequestType) {
@ -236,27 +288,9 @@ public class SamlService extends AuthorizationEndpointBase {
String issuer = requestAbstractType.getIssuer() == null ? null : issuerNameId.getValue();
ClientModel client = realm.getClientByClientId(issuer);
if (client == null) {
event.client(issuer);
event.error(Errors.CLIENT_NOT_FOUND);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.UNKNOWN_LOGIN_REQUESTER);
}
if (!client.isEnabled()) {
event.error(Errors.CLIENT_DISABLED);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.LOGIN_REQUESTER_NOT_ENABLED);
}
if (client.isBearerOnly()) {
event.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.BEARER_ONLY);
}
if (!client.isStandardFlowEnabled()) {
event.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.STANDARD_FLOW_DISABLED);
}
if (!isClientProtocolCorrect(client)) {
event.error(Errors.INVALID_CLIENT);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, "Wrong client protocol.");
Response error = checkClientValidity(client);
if (error != null) {
return error;
}
session.getContext().setClient(client);
@ -292,6 +326,79 @@ public class SamlService extends AuthorizationEndpointBase {
}
}
/**
* Handle a received artifact message. This means finding the client based on the content of the artifact,
* sending an ArtifactResolve, receiving an ArtifactResponse, and handling its content based on the "standard"
* workflows.
*
* @param artifact the received artifact
* @param relayState the current relay state
* @return a Response based on the content of the ArtifactResponse's content
*/
protected void handleArtifact(AsyncResponse asyncResponse, String artifact, String relayState) {
logger.tracef("Keycloak obtained artifact %s. %s", artifact, getShortStackTrace());
//Find client
ClientModel client;
try {
client = getArtifactResolver(artifact).selectSourceClient(artifact, realm.getClientsStream());
Response error = checkClientValidity(client);
if (error != null) {
asyncResponse.resume(error);
return;
}
} catch (ArtifactResolverProcessingException e) {
event.event(EventType.LOGIN);
event.detail(Details.REASON, e.getMessage());
event.error(Errors.INVALID_SAML_ARTIFACT);
asyncResponse.resume(ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST));
return;
}
try {
//send artifact resolve
Document doc = createArtifactResolve(client.getClientId(), artifact);
BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder();
SamlClient samlClient = new SamlClient(client);
if (samlClient.requiresRealmSignature()) {
KeyManager keyManager = session.keys();
KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm);
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
String canonicalization = samlClient.getCanonicalizationMethod();
if (canonicalization != null) {
binding.canonicalizationMethod(canonicalization);
}
binding.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument(doc);
}
String clientArtifactBindingURL = client.getAttribute(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE);
if (clientArtifactBindingURL == null || clientArtifactBindingURL.isEmpty()) {
throw new ConfigurationException("There is no configured artifact resolution service for the client " + client.getClientId());
}
URI clientArtifactBindingURI = new URI(clientArtifactBindingURL);
ExecutorService executor = session.getProvider(ExecutorsProvider.class).getExecutor("saml-artifact-pool");
ArtifactResolutionRunnable artifactResolutionRunnable = new ArtifactResolutionRunnable(getBindingType(), asyncResponse, doc, clientArtifactBindingURI, relayState, session.getContext().getConnection());
ScheduledTaskRunner task = new ScheduledTaskRunner(session.getKeycloakSessionFactory(), artifactResolutionRunnable);
executor.execute(task);
logger.tracef("ArtifactResolutionRunnable scheduled, current transaction will be rolled back");
// Current transaction must be ignored due to asyncResponse.
session.getTransactionManager().rollback();
} catch (URISyntaxException | ProcessingException | ParsingException | ConfigurationException e) {
event.event(EventType.LOGIN);
event.detail(Details.REASON, e.getMessage());
event.error(Errors.IDENTITY_PROVIDER_ERROR);
asyncResponse.resume(ErrorPage.error(session, null, Response.Status.INTERNAL_SERVER_ERROR, Messages.UNEXPECTED_ERROR_HANDLING_REQUEST));
return;
}
}
protected abstract String encodeSamlDocument(Document samlDocument) throws ProcessingException;
protected abstract void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException;
protected abstract boolean containsUnencryptedSignature(SAMLDocumentHolder documentHolder);
@ -315,7 +422,12 @@ public class SamlService extends AuthorizationEndpointBase {
if (redirectUri != null && ! "null".equals(redirectUri.toString())) { // "null" is for testing purposes
redirect = RedirectUtils.verifyRedirectUri(session, redirectUri.toString(), client);
} else {
if (bindingType.equals(SamlProtocol.SAML_POST_BINDING)) {
if ((requestAbstractType.getProtocolBinding() != null
&& JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri()
.equals(requestAbstractType.getProtocolBinding()))
|| samlClient.forceArtifactBinding()) {
redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE);
} else if (bindingType.equals(SamlProtocol.SAML_POST_BINDING)) {
redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE);
} else {
redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE);
@ -333,6 +445,14 @@ public class SamlService extends AuthorizationEndpointBase {
AuthenticationSessionModel authSession = createAuthenticationSession(client, relayState);
// determine if artifact binding should be used to answer the login request
if ((requestAbstractType.getProtocolBinding() != null
&& JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri()
.equals(requestAbstractType.getProtocolBinding()))
|| new SamlClient(client).forceArtifactBinding()) {
authSession.setClientNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get(), "true");
}
authSession.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
authSession.setRedirectUri(redirect);
authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
@ -365,15 +485,13 @@ public class SamlService extends AuthorizationEndpointBase {
NameIDType nameID = (NameIDType) baseID;
authSession.setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, nameID.getValue());
}
}
}
if (null != requestAbstractType.isForceAuthn()
&& requestAbstractType.isForceAuthn()) {
&& requestAbstractType.isForceAuthn()) {
authSession.setAuthNote(SamlProtocol.SAML_LOGIN_REQUEST_FORCEAUTHN, SamlProtocol.SAML_FORCEAUTHN_REQUIREMENT);
}
for(Iterator<SamlAuthenticationPreprocessor> it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext();) {
requestAbstractType = it.next().beforeProcessingLoginRequest(requestAbstractType, authSession);
@ -390,6 +508,8 @@ public class SamlService extends AuthorizationEndpointBase {
if (requestedProtocolBinding != null) {
if (JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get().equals(requestedProtocolBinding.toString())) {
return SamlProtocol.SAML_POST_BINDING;
} else if (JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get().equals(requestedProtocolBinding.toString())) {
return getBindingType();
} else {
return SamlProtocol.SAML_REDIRECT_BINDING;
}
@ -418,12 +538,12 @@ public class SamlService extends AuthorizationEndpointBase {
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, false);
if (authResult != null) {
String logoutBinding = getBindingType();
String postBindingUri = SamlProtocol.getLogoutServiceUrl(session, client, SamlProtocol.SAML_POST_BINDING);
String postBindingUri = SamlProtocol.getLogoutServiceUrl(session, client, SamlProtocol.SAML_POST_BINDING, false);
if (samlClient.forcePostBinding() && postBindingUri != null && ! postBindingUri.trim().isEmpty())
logoutBinding = SamlProtocol.SAML_POST_BINDING;
boolean postBinding = Objects.equals(SamlProtocol.SAML_POST_BINDING, logoutBinding);
String bindingUri = SamlProtocol.getLogoutServiceUrl(session, client, logoutBinding);
String bindingUri = SamlProtocol.getLogoutServiceUrl(session, client, logoutBinding, false);
UserSessionModel userSession = authResult.getSession();
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING_URI, bindingUri);
if (samlClient.requiresRealmSignature()) {
@ -432,6 +552,7 @@ public class SamlService extends AuthorizationEndpointBase {
}
if (relayState != null)
userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState);
userSession.setNote(SamlProtocol.SAML_LOGOUT_REQUEST_ID, logoutRequest.getID());
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING, logoutBinding);
userSession.setNote(SamlProtocol.SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO, Boolean.toString((! postBinding) && samlClient.addExtensionsElementWithKeyInfo()));
@ -442,12 +563,21 @@ public class SamlService extends AuthorizationEndpointBase {
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
if (clientSession != null) {
clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name());
//artifact binding state must be attached to the user session upon logout, as authenticated session
//no longer exists when the LogoutResponse message is sent
if ("true".equals(clientSession.getNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get()))
&& SamlProtocol.useArtifactForLogout(client)){
clientSession.setAction(AuthenticationSessionModel.Action.LOGGING_OUT.name());
userSession.setNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get(), "true");
userSession.setNote(SamlProtocol.SAML_LOGOUT_INITIATOR_CLIENT_ID, client.getId());
}
}
for(Iterator<SamlAuthenticationPreprocessor> it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext();) {
logoutRequest = it.next().beforeProcessingLogoutRequest(logoutRequest, userSession, clientSession);
}
logger.debug("browser Logout");
return authManager.browserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers, null);
} else if (logoutRequest.getSessionIndex() != null) {
@ -477,9 +607,8 @@ public class SamlService extends AuthorizationEndpointBase {
}
// default
String logoutBinding = getBindingType();
String logoutBindingUri = SamlProtocol.getLogoutServiceUrl(session, client, logoutBinding);
String logoutBindingUri = SamlProtocol.getLogoutServiceUrl(session, client, logoutBinding, true);
String logoutRelayState = relayState;
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
builder.logoutRequestID(logoutRequest.getID());
@ -532,16 +661,37 @@ public class SamlService extends AuthorizationEndpointBase {
}
}
public Response execute(String samlRequest, String samlResponse, String relayState) {
Response response = basicChecks(samlRequest, samlResponse);
public Response execute(String samlRequest, String samlResponse, String relayState, String artifact) {
Response response = basicChecks(samlRequest, samlResponse, artifact);
if (response != null)
return response;
if (samlRequest != null)
return handleSamlRequest(samlRequest, relayState);
else
return handleSamlResponse(samlResponse, relayState);
}
public void execute(AsyncResponse asyncReponse, String samlRequest, String samlResponse, String relayState, String artifact) {
Response response = basicChecks(samlRequest, samlResponse, artifact);
if (response != null){
asyncReponse.resume(response);
return;
}
if (artifact != null) {
handleArtifact(asyncReponse, artifact, relayState);
return;
}
if (samlRequest != null) {
asyncReponse.resume(handleSamlRequest(samlRequest, relayState));
return;
} else {
asyncReponse.resume(handleSamlResponse(samlResponse, relayState));
}
}
/**
* KEYCLOAK-12616, KEYCLOAK-12944: construct the expected destination URI using the configured base URI.
*
@ -557,6 +707,15 @@ public class SamlService extends AuthorizationEndpointBase {
protected class PostBindingProtocol extends BindingProtocol {
@Override
protected String encodeSamlDocument(Document samlDocument) throws ProcessingException {
try {
return PostBindingUtil.base64Encode(DocumentUtil.asString(samlDocument));
} catch (IOException e) {
throw new ProcessingException(e);
}
}
@Override
protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException {
SamlProtocolUtils.verifyDocumentSignature(client, documentHolder.getSamlDocument());
@ -588,6 +747,15 @@ public class SamlService extends AuthorizationEndpointBase {
protected class RedirectBindingProtocol extends BindingProtocol {
@Override
protected String encodeSamlDocument(Document samlDocument) throws ProcessingException {
try {
return RedirectBindingUtil.deflateBase64Encode(DocumentUtil.asString(samlDocument).getBytes(GeneralConstants.SAML_CHARSET_NAME));
} catch (IOException e) {
throw new ProcessingException(e);
}
}
@Override
protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException {
PublicKey publicKey = SamlProtocolUtils.getSignatureValidationKey(client);
@ -629,13 +797,22 @@ public class SamlService extends AuthorizationEndpointBase {
return handleBrowserAuthenticationRequest(authSession, samlProtocol, isPassive, redirectToAuthentication);
}
public RedirectBindingProtocol newRedirectBindingProtocol() {
return new RedirectBindingProtocol();
}
public PostBindingProtocol newPostBindingProtocol() {
return new PostBindingProtocol();
}
/**
*/
@GET
public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @QueryParam(GeneralConstants.RELAY_STATE) String relayState) {
public void redirectBinding(@Suspended AsyncResponse asyncResponse, @QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @QueryParam(GeneralConstants.RELAY_STATE) String relayState, @QueryParam(GeneralConstants.SAML_ARTIFACT_KEY) String artifact) {
logger.debug("SAML GET");
CacheControlUtil.noBackButtonCacheControlHeader();
return new RedirectBindingProtocol().execute(samlRequest, samlResponse, relayState);
new RedirectBindingProtocol().execute(asyncResponse, samlRequest, samlResponse, relayState, artifact);
}
/**
@ -643,14 +820,14 @@ public class SamlService extends AuthorizationEndpointBase {
@POST
@NoCache
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @FormParam(GeneralConstants.RELAY_STATE) String relayState) {
public void postBinding(@Suspended AsyncResponse asyncResponse, @FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @FormParam(GeneralConstants.RELAY_STATE) String relayState, @FormParam(GeneralConstants.SAML_ARTIFACT_KEY) String artifact) {
logger.debug("SAML POST");
PostBindingProtocol postBindingProtocol = new PostBindingProtocol();
// this is to support back button on browser
// if true, we redirect to authenticate URL otherwise back button behavior has bad side effects
// and we want to turn it off.
postBindingProtocol.redirectToAuthentication = true;
return postBindingProtocol.execute(samlRequest, samlResponse, relayState);
postBindingProtocol.execute(asyncResponse, samlRequest, samlResponse, relayState, artifact);
}
@GET
@ -680,6 +857,8 @@ public class SamlService extends AuthorizationEndpointBase {
RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL),
RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL),
RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL),
RealmsResource.protocolUrl(uriInfo).path(SamlService.ARTIFACT_RESOLUTION_SERVICE_PATH)
.build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL),
RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString(),
true,
signingKeys);
@ -703,6 +882,37 @@ public class SamlService extends AuthorizationEndpointBase {
return false;
}
private Response checkClientValidity(ClientModel client) {
if (client == null) {
event.event(EventType.LOGIN);
event.detail(Details.REASON, "Cannot_match_source_hash");
event.error(Errors.CLIENT_NOT_FOUND);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
}
if (!client.isEnabled()) {
event.event(EventType.LOGIN);
event.error(Errors.CLIENT_DISABLED);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.LOGIN_REQUESTER_NOT_ENABLED);
}
if (client.isBearerOnly()) {
event.event(EventType.LOGIN);
event.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.BEARER_ONLY);
}
if (!client.isStandardFlowEnabled()) {
event.event(EventType.LOGIN);
event.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.STANDARD_FLOW_DISABLED);
}
if (!isClientProtocolCorrect(client)) {
event.event(EventType.LOGIN);
event.error(Errors.INVALID_CLIENT);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, "Wrong client protocol.");
}
return null;
}
@GET
@Path("clients/{client}")
@Produces(MediaType.TEXT_HTML_UTF_8)
@ -802,7 +1012,62 @@ public class SamlService extends AuthorizationEndpointBase {
return authSession;
}
/**
* Handles SOAP messages. Chooses the correct response path depending on whether the message is of type ECP or Artifact
* @param inputStream the data of the request.
* @return The response to the SOAP message
*/
@POST
@Path(ARTIFACT_RESOLUTION_SERVICE_PATH)
@NoCache
@Consumes({"application/soap+xml", MediaType.TEXT_XML})
public Response artifactResolutionService(InputStream inputStream) {
Document soapBodyContents = Soap.extractSoapMessage(inputStream);
ArtifactResolveType artifactResolveType = null;
SAMLDocumentHolder samlDocumentHolder = null;
try {
samlDocumentHolder = SAML2Request.getSAML2ObjectFromDocument(soapBodyContents);
if (samlDocumentHolder.getSamlObject() instanceof ArtifactResolveType) {
logger.debug("Received artifact resolve message");
artifactResolveType = (ArtifactResolveType)samlDocumentHolder.getSamlObject();
}
} catch (Exception e) {
logger.errorf("Artifact resolution endpoint obtained request that contained no " +
"ArtifactResolve message: %s", DocumentUtil.asString(soapBodyContents));
return Soap.createFault().reason("").detail("").build();
}
if (artifactResolveType == null) {
logger.errorf("Artifact resolution endpoint obtained request that contained no " +
"ArtifactResolve message: %s", DocumentUtil.asString(soapBodyContents));
return Soap.createFault().reason("").detail("").build();
}
try {
return artifactResolve(artifactResolveType, samlDocumentHolder);
} catch (Exception e) {
try {
return emptyArtifactResponseMessage(artifactResolveType, null, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.getUri());
} catch (ConfigurationException | ProcessingException configurationException) {
String reason = "An error occurred while trying to return the artifactResponse";
String detail = e.getMessage();
if (detail == null) {
detail = "";
}
logger.errorf("Failure during ArtifactResolve reason: %s, detail: %s", reason, detail);
return Soap.createFault().reason(reason).detail(detail).build();
}
}
}
/**
* Handles SOAP messages. Chooses the correct response path depending on whether the message is of type ECP
* @param inputStream the data of the request.
* @return The response to the SOAP message
*/
@POST
@NoCache
@Consumes({"application/soap+xml",MediaType.TEXT_XML})
@ -813,4 +1078,373 @@ public class SamlService extends AuthorizationEndpointBase {
return bindingService.authenticate(inputStream);
}
private ClientModel getAndCheckClientModel(String clientSessionId, String clientId) throws ProcessingException {
ClientModel client = session.clients().getClientById(realm, clientSessionId);
if (client == null) {
throw new ProcessingException(Errors.CLIENT_NOT_FOUND);
}
if (!client.isEnabled()) {
throw new ProcessingException(Errors.CLIENT_DISABLED);
}
if (client.isBearerOnly()) {
throw new ProcessingException(Errors.NOT_ALLOWED);
}
if (!client.isStandardFlowEnabled()) {
throw new ProcessingException(Errors.NOT_ALLOWED);
}
if (!client.getClientId().equals(clientId)) {
logger.errorf("Resolve message with wrong issuer. Artifact was issued for client %s, " +
"however ArtifactResolveMessage came from client %s.", client.getClientId(), clientId);
throw new ProcessingException(Errors.INVALID_SAML_ARTIFACT);
}
return client;
}
private SamlArtifactSessionMappingStoreProvider getArtifactSessionMappingStore() {
return session.getProvider(SamlArtifactSessionMappingStoreProvider.class);
}
/**
* Takes an artifact resolve message and returns the artifact response, if the artifact is found belonging to a session
* of the issuer.
* @param artifactResolveMessage The artifact resolve message sent by the client
* @param artifactResolveHolder the document containing the artifact resolve message sent by the client
* @return a Response containing the SOAP message with the ArifactResponse
* @throws ParsingException
* @throws ConfigurationException
* @throws ProcessingException
*/
public Response artifactResolve(ArtifactResolveType artifactResolveMessage, SAMLDocumentHolder artifactResolveHolder) throws ParsingException, ConfigurationException, ProcessingException {
logger.debug("Received artifactResolve message for artifact " + artifactResolveMessage.getArtifact() + "\n" +
"Message: \n" + DocumentUtil.getDocumentAsString(artifactResolveHolder.getSamlDocument()));
String artifact = artifactResolveMessage.getArtifact(); // Artifact from resolve request
if (artifact == null) {
logger.errorf("Artifact to resolve was null");
return emptyArtifactResponseMessage(artifactResolveMessage, null, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.getUri());
}
ArtifactResolver artifactResolver = getArtifactResolver(artifact);
if (artifactResolver == null) {
logger.errorf("Cannot find ArtifactResolver for artifact %s", artifact);
return emptyArtifactResponseMessage(artifactResolveMessage, null, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.getUri());
}
// Obtain details of session that issued artifact and check if it corresponds to issuer of Resolve message
SamlArtifactSessionMappingModel sessionMapping = getArtifactSessionMappingStore().get(artifact);
if (sessionMapping == null) {
logger.errorf("No data stored for artifact %s", artifact);
return emptyArtifactResponseMessage(artifactResolveMessage, null);
}
UserSessionModel userSessionModel = session.sessions().getUserSession(realm, sessionMapping.getUserSessionId());
if (userSessionModel == null) {
logger.errorf("UserSession with id: %s, that corresponds to artifact: %s does not exist.", sessionMapping.getUserSessionId(), artifact);
return emptyArtifactResponseMessage(artifactResolveMessage, null);
}
AuthenticatedClientSessionModel clientSessionModel = userSessionModel.getAuthenticatedClientSessions().get(sessionMapping.getClientSessionId());
if (clientSessionModel == null) {
logger.errorf("ClientSession with id: %s, that corresponds to artifact: %s and UserSession: %s does not exist.", sessionMapping.getClientSessionId(), artifact, sessionMapping.getUserSessionId());
return emptyArtifactResponseMessage(artifactResolveMessage, null);
}
ClientModel clientModel = getAndCheckClientModel(sessionMapping.getClientSessionId(), artifactResolveMessage.getIssuer().getValue());
SamlClient samlClient = new SamlClient(clientModel);
// Check signature within ArtifactResolve request if client requires it
if (samlClient.requiresClientSignature()) {
try {
SamlProtocolUtils.verifyDocumentSignature(clientModel, artifactResolveHolder.getSamlDocument());
} catch (VerificationException e) {
SamlService.logger.error("request validation failed", e);
return emptyArtifactResponseMessage(artifactResolveMessage, clientModel);
}
}
// Obtain artifactResponse from clientSessionModel
String artifactResponseString;
try {
artifactResponseString = artifactResolver.resolveArtifact(clientSessionModel, artifact);
} catch (ArtifactResolverProcessingException e) {
logger.errorf(e, "Failed to resolve artifact: %s.", artifact);
return emptyArtifactResponseMessage(artifactResolveMessage, clientModel);
}
// Artifact is successfully resolved, we can remove session mapping from storage
getArtifactSessionMappingStore().remove(artifact);
Document artifactResponseDocument = null;
ArtifactResponseType artifactResponseType = null;
try {
SAMLDataMarshaller marshaller = new SAMLDataMarshaller();
artifactResponseType = marshaller.deserialize(artifactResponseString, ArtifactResponseType.class);
artifactResponseDocument = SamlProtocolUtils.convert(artifactResponseType);
} catch (ParsingException | ConfigurationException | ProcessingException e) {
logger.errorf(e,"Failed to obtain document from ArtifactResponseString: %s.", artifactResponseString);
return emptyArtifactResponseMessage(artifactResolveMessage, clientModel);
}
// If clientSession is in LOGGING_OUT action, now we can move it to LOGGED_OUT
if (CommonClientSessionModel.Action.LOGGING_OUT.name().equals(clientSessionModel.getAction())) {
clientSessionModel.setAction(CommonClientSessionModel.Action.LOGGED_OUT.name());
// If Keycloak sent LogoutResponse we need to also remove UserSession
if (artifactResponseType.getAny() instanceof StatusResponseType
&& artifactResponseString.contains(JBossSAMLConstants.LOGOUT_RESPONSE.get())) {
if (!UserSessionModel.State.LOGGED_OUT_UNCONFIRMED.equals(userSessionModel.getState())) {
logger.warnf("Keycloak issued LogoutResponse for clientSession %s, however user session %s was not in LOGGED_OUT_UNCONFIRMED state.",
clientSessionModel.getId(), userSessionModel.getId());
}
AuthenticationManager.finishUnconfirmedUserSession(session, realm, userSessionModel);
}
}
return artifactResponseMessage(artifactResolveMessage, artifactResponseDocument, clientModel);
}
private Response emptyArtifactResponseMessage(ArtifactResolveType artifactResolveMessage, ClientModel clientModel) throws ProcessingException, ConfigurationException {
return emptyArtifactResponseMessage(artifactResolveMessage, clientModel, JBossSAMLURIConstants.STATUS_SUCCESS.getUri());
}
private Response emptyArtifactResponseMessage(ArtifactResolveType artifactResolveMessage, ClientModel clientModel, URI responseStatusCode) throws ProcessingException, ConfigurationException {
ArtifactResponseType artifactResponse = SamlProtocolUtils.buildArtifactResponse(null, SAML2NameIDBuilder.value(
RealmsResource.realmBaseUrl(session.getContext().getUri()).build(realm.getName()).toString()).build(), responseStatusCode);
Document artifactResponseDocument;
try {
artifactResponseDocument = SamlProtocolUtils.convert(artifactResponse);
} catch (ParsingException | ConfigurationException | ProcessingException e) {
logger.errorf("Failed to obtain document from ArtifactResponse: %s.", artifactResponse);
throw new ProcessingException(Errors.INVALID_SAML_ARTIFACT_RESPONSE, e);
}
return artifactResponseMessage(artifactResolveMessage, artifactResponseDocument, clientModel);
}
private Response artifactResponseMessage(ArtifactResolveType artifactResolveMessage, Document artifactResponseDocument, ClientModel clientModel) throws ProcessingException, ConfigurationException {
// Add "inResponseTo" to artifactResponse
if (artifactResolveMessage.getID() != null && !artifactResolveMessage.getID().trim().isEmpty()){
Element artifactResponseElement = artifactResponseDocument.getDocumentElement();
artifactResponseElement.setAttribute("InResponseTo", artifactResolveMessage.getID());
}
JaxrsSAML2BindingBuilder bindingBuilder = new JaxrsSAML2BindingBuilder(session);
if (clientModel != null) {
SamlClient samlClient = new SamlClient(clientModel);
// Sign document/assertion if necessary, necessary to do this here, as the "inResponseTo" can only be set at this point
if (samlClient.requiresRealmSignature() || samlClient.requiresAssertionSignature()) {
KeyManager keyManager = session.keys();
KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm);
String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
String canonicalization = samlClient.getCanonicalizationMethod();
if (canonicalization != null) {
bindingBuilder.canonicalizationMethod(canonicalization);
}
bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate());
if (samlClient.requiresRealmSignature()) bindingBuilder.signDocument();
if (samlClient.requiresAssertionSignature()) bindingBuilder.signAssertions();
}
// Encrypt assertion if client requires it
if (samlClient.requiresEncryption()) {
PublicKey publicKey = null;
try {
publicKey = SamlProtocolUtils.getEncryptionKey(clientModel);
} catch (Exception e) {
logger.error("Failed to obtain encryption key for client", e);
return emptyArtifactResponseMessage(artifactResolveMessage, null);
}
bindingBuilder.encrypt(publicKey);
}
}
bindingBuilder.postBinding(artifactResponseDocument);
Soap.SoapMessageBuilder messageBuilder = Soap.createMessage();
messageBuilder.addToBody(artifactResponseDocument);
if (logger.isDebugEnabled()) {
String artifactResponse = DocumentUtil.asString(artifactResponseDocument);
logger.debugf("Sending artifactResponse message for artifact %s. Message: \n %s", artifactResolveMessage.getArtifact(), artifactResponse);
}
return messageBuilder.build();
}
/**
* Creates an ArtifactResolve document with the given issuer and artifact
* @param issuer the value to set as "issuer"
* @param artifact the value to set as "artifact"
* @return the Document of the created ArtifactResolve message
* @throws ProcessingException
* @throws ParsingException
* @throws ConfigurationException
*/
private Document createArtifactResolve(String issuer, String artifact) throws ProcessingException, ParsingException, ConfigurationException {
ArtifactResolveType artifactResolve = new ArtifactResolveType(IDGenerator.create("ID_"),
XMLTimeUtil.getIssueInstant());
NameIDType nameIDType = new NameIDType();
nameIDType.setValue(issuer);
artifactResolve.setIssuer(nameIDType);
artifactResolve.setArtifact(artifact);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
XMLStreamWriter xmlStreamWriter = StaxUtil.getXMLStreamWriter(bos);
new SAMLRequestWriter(xmlStreamWriter).write(artifactResolve);
return DocumentUtil.getDocument(new ByteArrayInputStream(bos.toByteArray()));
}
private ArtifactResolver getArtifactResolver(String artifact) {
ArtifactResolver artifactResolver = session.getProvider(ArtifactResolver.class, ArtifactBindingUtils.artifactToResolverProviderId(artifact));
return artifactResolver != null ? artifactResolver : session.getProvider(ArtifactResolver.class);
}
private class ArtifactResolutionRunnable implements ScheduledTask{
private AsyncResponse asyncResponse;
private URI clientArtifactBindingURI;
private String relayState;
private Document doc;
private UriInfo uri;
private String realmId;
private HttpHeaders httpHeaders;
private ClientConnection connection;
private org.jboss.resteasy.spi.HttpResponse response;
private HttpRequest request;
private String bindingType;
public ArtifactResolutionRunnable(String bindingType, AsyncResponse asyncResponse, Document doc, URI clientArtifactBindingURI, String relayState, ClientConnection connection){
this.asyncResponse = asyncResponse;
this.doc = doc;
this.clientArtifactBindingURI = clientArtifactBindingURI;
this.relayState = relayState;
this.uri = session.getContext().getUri();
this.realmId = realm.getId();
this.httpHeaders = new ResteasyHttpHeaders(headers.getRequestHeaders());
this.connection = connection;
this.response = Resteasy.getContextData(org.jboss.resteasy.spi.HttpResponse.class);
this.request = Resteasy.getContextData(HttpRequest.class);
this.bindingType = bindingType;
}
public void run(KeycloakSession session){
// Initialize context
Resteasy.pushContext(UriInfo.class, uri);
KeycloakTransaction tx = session.getTransactionManager();
Resteasy.pushContext(KeycloakTransaction.class, tx);
Resteasy.pushContext(KeycloakSession.class, session);
Resteasy.pushContext(HttpHeaders.class, httpHeaders);
Resteasy.pushContext(org.jboss.resteasy.spi.HttpResponse.class, response);
Resteasy.pushContext(HttpRequest.class, request);
Resteasy.pushContext(ClientConnection.class, connection);
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.getRealmByName(realmId);
if (realm == null) {
throw new NotFoundException("Realm does not exist");
}
session.getContext().setRealm(realm);
EventBuilder event = new EventBuilder(realm, session, clientConnection);
// Call Artifact Resolution Service
HttpClientProvider httpClientProvider = session.getProvider(HttpClientProvider.class);
CloseableHttpClient httpClient = httpClientProvider.getHttpClient();
HttpPost httpPost = Soap.createMessage().addToBody(doc).buildHttpPost(clientArtifactBindingURI);
if (logger.isTraceEnabled()) {
logger.tracef("Resolving artifact %s", DocumentUtil.asString(doc));
}
try (CloseableHttpResponse result = httpClient.execute(httpPost)) {
try {
if (result.getStatusLine().getStatusCode() != Response.Status.OK.getStatusCode()) {
throw new ProcessingException(String.format("Artifact resolution failed with status: %d", result.getStatusLine().getStatusCode()));
}
Document soapBodyContents = Soap.extractSoapMessage(result.getEntity().getContent());
SAMLDocumentHolder samlDoc = SAML2Request.getSAML2ObjectFromDocument(soapBodyContents);
if (!(samlDoc.getSamlObject() instanceof ArtifactResponseType)) {
throw new ProcessingException("Message received from ArtifactResolveService is not an ArtifactResponseMessage");
}
if (logger.isTraceEnabled()) {
logger.tracef("Resolved object: %s" + DocumentUtil.asString(samlDoc.getSamlDocument()));
}
ArtifactResponseType art = (ArtifactResponseType) samlDoc.getSamlObject();
if (art.getAny() == null) {
AsyncResponseTransaction.finishAsyncResponseInTransaction(session, asyncResponse,
ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.ARTIFACT_RESOLUTION_SERVICE_INVALID_RESPONSE));
return;
}
LoginProtocolFactory factory = (LoginProtocolFactory) session.getKeycloakSessionFactory().getProviderFactory(LoginProtocol.class, "saml");
if (factory == null) {
logger.debugf("protocol %s not found", "saml");
throw new NotFoundException("Protocol not found");
}
SamlService endpoint = (SamlService) factory.createProtocolEndpoint(realm, event);
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
BindingProtocol protocol;
if (SamlProtocol.SAML_POST_BINDING.equals(bindingType)) {
protocol = endpoint.newPostBindingProtocol();
} else if (SamlProtocol.SAML_REDIRECT_BINDING.equals(bindingType)) {
protocol = endpoint.newRedirectBindingProtocol();
} else {
throw new ConfigurationException("Invalid binding protocol: " + bindingType);
}
if (art.getAny() instanceof ResponseType) {
Document clientMessage = SAML2Request.convert((ResponseType) art.getAny());
String response = protocol.encodeSamlDocument(clientMessage);
AsyncResponseTransaction.finishAsyncResponseInTransaction(session, asyncResponse,
protocol.handleSamlResponse(response, relayState));
} else if (art.getAny() instanceof RequestAbstractType) {
Document clientMessage = SAML2Request.convert((RequestAbstractType) art.getAny());
String request = protocol.encodeSamlDocument(clientMessage);
AsyncResponseTransaction.finishAsyncResponseInTransaction(session, asyncResponse,
protocol.handleSamlRequest(request, relayState));
} else {
throw new ProcessingException("Cannot recognise message contained in ArtifactResponse");
}
} finally {
EntityUtils.consumeQuietly(result.getEntity());
}
} catch (IOException | ProcessingException | ParsingException e) {
event.event(EventType.LOGIN);
event.detail(Details.REASON, e.getMessage());
event.error(Errors.IDENTITY_PROVIDER_ERROR);
AsyncResponseTransaction.finishAsyncResponseInTransaction(session, asyncResponse,
ErrorPage.error(session, null, Response.Status.INTERNAL_SERVER_ERROR, Messages.ARTIFACT_RESOLUTION_SERVICE_INVALID_RESPONSE));
} catch(ConfigurationException e) {
event.event(EventType.LOGIN);
event.detail(Details.REASON, e.getMessage());
event.error(Errors.IDENTITY_PROVIDER_ERROR);
AsyncResponseTransaction.finishAsyncResponseInTransaction(session, asyncResponse,
ErrorPage.error(session, null, Response.Status.INTERNAL_SERVER_ERROR, Messages.UNEXPECTED_ERROR_HANDLING_REQUEST));
}
}
}
}

View file

@ -54,25 +54,43 @@ public class SamlSPDescriptorClientInstallation implements ClientInstallationPro
SamlClient samlClient = new SamlClient(client);
String assertionUrl;
String logoutUrl;
URI binding;
URI loginBinding;
URI logoutBinding = null;
if (samlClient.forcePostBinding()) {
assertionUrl = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE);
logoutUrl = client.getAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE);
binding = JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.getUri();
loginBinding = JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.getUri();
} else { //redirect binding
assertionUrl = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE);
logoutUrl = client.getAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE);
binding = JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri();
loginBinding = JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri();
}
if (samlClient.forceArtifactBinding()) {
if (client.getAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE) != null) {
logoutBinding = JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri();
logoutUrl = client.getAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE);
} else {
logoutBinding = loginBinding;
}
assertionUrl = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE);
loginBinding = JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri();
}
if (assertionUrl == null || assertionUrl.trim().isEmpty()) assertionUrl = client.getManagementUrl();
if (assertionUrl == null || assertionUrl.trim().isEmpty()) assertionUrl = FALLBACK_ERROR_URL_STRING;
if (logoutUrl == null || logoutUrl.trim().isEmpty()) logoutUrl = client.getManagementUrl();
if (logoutUrl == null || logoutUrl.trim().isEmpty()) logoutUrl = FALLBACK_ERROR_URL_STRING;
if (logoutBinding == null) logoutBinding = loginBinding;
String nameIdFormat = samlClient.getNameIDFormat();
if (nameIdFormat == null) nameIdFormat = SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT;
Element spCertificate = SPMetadataDescriptor.buildKeyInfoElement(null, samlClient.getClientSigningCertificate());
Element encCertificate = SPMetadataDescriptor.buildKeyInfoElement(null, samlClient.getClientEncryptingCertificate());
return SPMetadataDescriptor.getSPDescriptor(binding, new URI(assertionUrl), new URI(logoutUrl), samlClient.requiresClientSignature(),
return SPMetadataDescriptor.getSPDescriptor(loginBinding, logoutBinding, new URI(assertionUrl), new URI(logoutUrl), samlClient.requiresClientSignature(),
samlClient.requiresAssertionSignature(), samlClient.requiresEncryption(),
client.getClientId(), nameIdFormat, Arrays.asList(spCertificate), Arrays.asList(encCertificate));
} catch (Exception ex) {

View file

@ -29,7 +29,7 @@ import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder;
import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.protocol.saml.SamlService;
import org.keycloak.protocol.saml.profile.ecp.util.Soap;
import org.keycloak.protocol.saml.profile.util.Soap;
import org.keycloak.saml.SAML2LogoutResponseBuilder;
import org.keycloak.saml.common.constants.JBossSAMLConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
@ -61,6 +61,10 @@ public class SamlEcpProfileService extends SamlService {
}
public Response authenticate(InputStream inputStream) {
return authenticate(Soap.extractSoapMessage(inputStream));
}
public Response authenticate(Document soapMessage) {
try {
return new PostBindingProtocol() {
@Override
@ -80,7 +84,7 @@ public class SamlEcpProfileService extends SamlService {
requestAbstractType.setDestination(session.getContext().getUri().getAbsolutePath());
return super.loginRequest(relayState, requestAbstractType, client);
}
}.execute(Soap.toSamlHttpPostMessage(inputStream), null, null);
}.execute(Soap.toSamlHttpPostMessage(soapMessage), null, null, null);
} catch (Exception e) {
String reason = "Some error occurred while processing the AuthnRequest.";
String detail = e.getMessage();

View file

@ -15,18 +15,24 @@
* limitations under the License.
*/
package org.keycloak.protocol.saml.profile.ecp.util;
package org.keycloak.protocol.saml.profile.util;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.keycloak.saml.processing.core.saml.v2.util.DocumentUtil;
import org.keycloak.saml.processing.web.util.PostBindingUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.Name;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPConnection;
import javax.xml.soap.SOAPConnectionFactory;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPFault;
@ -34,6 +40,7 @@ import javax.xml.soap.SOAPHeaderElement;
import javax.xml.soap.SOAPMessage;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URI;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -54,21 +61,50 @@ public final class Soap {
*
* <p>The resulting string is based on the Body of the SOAP message, which should map to a valid SAML message.
*
* @param inputStream the input stream containing a valid SOAP message with a Body that contains a SAML message
* @param document the document containing a valid SOAP message with a Body that contains a SAML message
*
* @return a string encoded accordingly with the SAML HTTP POST Binding specification
*/
public static String toSamlHttpPostMessage(InputStream inputStream) {
public static String toSamlHttpPostMessage(Document document) {
try {
return PostBindingUtil.base64Encode(DocumentUtil.asString(document));
} catch (Exception e) {
throw new RuntimeException("Error encoding SOAP document to String.", e);
}
}
/**
* <p>Returns Docuemnt based on the given <code>inputStream</code> which must contain a valid SOAP message.
*
* <p>The resulting string is based on the Body of the SOAP message, which should map to a valid SAML message.
*
* @param inputStream an InputStream consisting of a SOAPMessage
* @return A document containing the body of the SOAP message
*/
public static Document extractSoapMessage(InputStream inputStream) {
try {
MessageFactory messageFactory = MessageFactory.newInstance();
SOAPMessage soapMessage = messageFactory.createMessage(null, inputStream);
return extractSoapMessage(soapMessage);
} catch (Exception e) {
throw new RuntimeException("Error creating fault message.", e);
}
}
/**
* <p>Returns Docuemnt based on the given SOAP message.
*
* <p>The resulting string is based on the Body of the SOAP message, which should map to a valid SAML message.
* @param soapMessage a SOAPMessage from which to extract the body
* @return A document containing the body of the SOAP message
*/
public static Document extractSoapMessage(SOAPMessage soapMessage) {
try {
SOAPBody soapBody = soapMessage.getSOAPBody();
Node authnRequestNode = soapBody.getFirstChild();
Document document = DocumentUtil.createDocument();
document.appendChild(document.importNode(authnRequestNode, true));
return PostBindingUtil.base64Encode(DocumentUtil.asString(document));
return document;
} catch (Exception e) {
throw new RuntimeException("Error creating fault message.", e);
}
@ -123,11 +159,7 @@ public final class Soap {
}
}
public Response build() {
return build(Status.OK);
}
Response build(Status status) {
public byte[] getBytes() {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
@ -135,11 +167,55 @@ public final class Soap {
} catch (Exception e) {
throw new RuntimeException("Error while building SOAP Fault.", e);
}
return Response.status(status).entity(outputStream.toByteArray()).build();
return outputStream.toByteArray();
}
SOAPMessage getMessage() {
public Response build() {
return build(Status.OK);
}
/**
* Standard build method, generates a javax ws rs Response
* @param status the status of the response
* @return a Response containing the SOAP message
*/
Response build(Status status) {
return Response.status(status).entity(getBytes()).type(MediaType.TEXT_XML_TYPE).build();
}
/**
* Build method for testing, generates an appache httpcomponents HttpPost
* @param uri the URI to which to POST the soap message
* @return an HttpPost containing the SOAP message
*/
public HttpPost buildHttpPost(URI uri) {
HttpPost post = new HttpPost(uri);
post.setEntity(new ByteArrayEntity(getBytes(), ContentType.TEXT_XML));
return post;
}
/**
* Performs a synchronous call, sending the current message to the given url
* @param url a SOAP endpoint url
* @return the SOAPMessage returned by the contacted SOAP server
* @throws SOAPException Raised if there's a problem performing the SOAP call
*/
public SOAPMessage call(String url) throws SOAPException {
SOAPMessage response;
SOAPConnection soapConnection = null;
try {
SOAPConnectionFactory soapConnectionFactory = SOAPConnectionFactory.newInstance();
soapConnection = soapConnectionFactory.createConnection();
response = soapConnection.call(message, url);
} finally {
if (soapConnection != null) {
soapConnection.close();
}
}
return response;
}
public SOAPMessage getMessage() {
return this.message;
}
}

View file

@ -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]);
}
}

View file

@ -68,6 +68,7 @@ import org.keycloak.protocol.oidc.BackchannelLogoutResponse;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.saml.SamlClient;
import org.keycloak.representations.AccessToken;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
@ -387,6 +388,7 @@ public class AuthenticationManager {
Set<AuthenticatedClientSessionModel> notLoggedOutSessions = acs.entrySet().stream()
.filter(me -> ! Objects.equals(AuthenticationSessionModel.Action.LOGGED_OUT, getClientLogoutAction(logoutAuthSession, me.getKey())))
.filter(me -> ! Objects.equals(AuthenticationSessionModel.Action.LOGGED_OUT.name(), me.getValue().getAction()))
.filter(me -> ! Objects.equals(AuthenticationSessionModel.Action.LOGGING_OUT.name(), me.getValue().getAction()))
.filter(me -> Objects.nonNull(me.getValue().getProtocol())) // Keycloak service-like accounts
.map(Map.Entry::getValue)
.collect(Collectors.toSet());
@ -473,7 +475,7 @@ public class AuthenticationManager {
UserSessionModel userSession = clientSession.getUserSession();
ClientModel client = clientSession.getClient();
if (! client.isFrontchannelLogout() || AuthenticationSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) {
if (!client.isFrontchannelLogout() || AuthenticationSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) {
return null;
}
@ -500,7 +502,9 @@ public class AuthenticationManager {
logger.debug("returning frontchannel logout request to client");
// setting this to logged out cuz I'm not sure protocols can always verify that the client was logged out or not
setClientLogoutAction(logoutAuthSession, client.getId(), AuthenticationSessionModel.Action.LOGGED_OUT);
if (!AuthenticationSessionModel.Action.LOGGING_OUT.name().equals(clientSession.getAction())) {
setClientLogoutAction(logoutAuthSession, client.getId(), AuthenticationSessionModel.Action.LOGGED_OUT);
}
return response;
}
@ -599,7 +603,8 @@ public class AuthenticationManager {
private static Response browserLogoutAllClients(UserSessionModel userSession, KeycloakSession session, RealmModel realm, HttpHeaders headers, UriInfo uriInfo, AuthenticationSessionModel logoutAuthSession) {
Map<Boolean, List<AuthenticatedClientSessionModel>> acss = userSession.getAuthenticatedClientSessions().values().stream()
.filter(clientSession -> ! Objects.equals(AuthenticationSessionModel.Action.LOGGED_OUT.name(), clientSession.getAction()))
.filter(clientSession -> !Objects.equals(AuthenticationSessionModel.Action.LOGGED_OUT.name(), clientSession.getAction())
&& !Objects.equals(AuthenticationSessionModel.Action.LOGGING_OUT.name(), clientSession.getAction()))
.filter(clientSession -> clientSession.getProtocol() != null)
.collect(Collectors.partitioningBy(clientSession -> clientSession.getClient().isFrontchannelLogout()));
@ -623,9 +628,10 @@ public class AuthenticationManager {
checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession);
// For resolving artifact we don't need any cookie, all details are stored in session storage so we can remove
expireIdentityCookie(realm, uriInfo, connection);
expireRememberMeCookie(realm, uriInfo, connection);
userSession.setState(UserSessionModel.State.LOGGED_OUT);
String method = userSession.getNote(KEYCLOAK_LOGOUT_PROTOCOL);
EventBuilder event = new EventBuilder(realm, session, connection);
LoginProtocol protocol = session.getProvider(LoginProtocol.class, method);
@ -633,11 +639,50 @@ public class AuthenticationManager {
.setHttpHeaders(headers)
.setUriInfo(uriInfo)
.setEventBuilder(event);
Response response = protocol.finishLogout(userSession);
session.sessions().removeUserSession(realm, userSession);
// It may be possible that there are some client sessions that are still in LOGGING_OUT state
long numberOfUnconfirmedSessions = userSession.getAuthenticatedClientSessions().values().stream()
.filter(clientSessionModel -> CommonClientSessionModel.Action.LOGGING_OUT.name().equals(clientSessionModel.getAction()))
.count();
// If logout flow end up correctly there should be at maximum 1 client session in LOGGING_OUT action, if there are more, something went wrong
if (numberOfUnconfirmedSessions > 1) {
logger.warnf("There are more than one clientSession in logging_out state. Perhaps some client did not finish logout flow correctly.");
}
// By setting LOGGED_OUT_UNCONFIRMED state we are saying that anybody who will turn the last client session into
// LOGGED_OUT action can remove UserSession
if (numberOfUnconfirmedSessions >= 1) {
userSession.setState(UserSessionModel.State.LOGGED_OUT_UNCONFIRMED);
} else {
userSession.setState(UserSessionModel.State.LOGGED_OUT);
}
// Do not remove user session, it will be removed when last clientSession will be logged out
if (numberOfUnconfirmedSessions < 1) {
session.sessions().removeUserSession(realm, userSession);
}
session.authenticationSessions().removeRootAuthenticationSession(realm, logoutAuthSession.getParentSession());
return response;
}
public static void finishUnconfirmedUserSession(KeycloakSession session, RealmModel realm, UserSessionModel userSessionModel) {
if (userSessionModel.getAuthenticatedClientSessions().values().stream().anyMatch(cs -> !CommonClientSessionModel.Action.LOGGED_OUT.name().equals(cs.getAction()))) {
logger.warnf("UserSession with id %s is removed while there are still some user sessions that are not logged out properly.", userSessionModel.getId());
if (logger.isTraceEnabled()) {
logger.trace("Client sessions with their states:");
userSessionModel.getAuthenticatedClientSessions().values()
.forEach(clientSessionModel -> logger.tracef("Client session for clientId: %s has action: %s", clientSessionModel.getClient().getClientId(), clientSessionModel.getAction()));
}
}
session.sessions().removeUserSession(realm, userSessionModel);
}
public static IdentityCookieToken createIdentityToken(KeycloakSession keycloakSession, RealmModel realm, UserModel user, UserSessionModel session, String issuer) {

View file

@ -246,6 +246,9 @@ public class Messages {
public static final String DELEGATION_FAILED = "delegationFailedMessage";
public static final String DELEGATION_FAILED_HEADER = "delegationFailedHeader";
public static final String ARTIFACT_RESOLUTION_SERVICE_ERROR = "artifactResolutionServiceError";
public static final String ARTIFACT_RESOLUTION_SERVICE_INVALID_RESPONSE = "saml.artifactResolutionServiceInvalidResponse";
// WebAuthn
public static final String WEBAUTHN_REGISTER_TITLE = "webauthn-registration-title";
public static final String WEBAUTHN_LOGIN_TITLE = "webauthn-login-title";

View file

@ -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;
}
}

View file

@ -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

View file

@ -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));
}
}

View file

@ -22,3 +22,7 @@ echo ** Adding spi=userProfile with legacy-user-profile configuration of read-on
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:add(properties={},enabled=true)
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:map-put(name=properties,key=read-only-attributes,value=[deniedFoo,deniedBar*,deniedSome/thing,deniedsome*thing])
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:map-put(name=properties,key=admin-read-only-attributes,value=[deniedSomeAdmin])
echo ** Do not reuse connections for HttpClientProvider within testsuite **
/subsystem=keycloak-server/spi=connectionsHttpClient/provider=default/:map-put(name=properties,key=reuse-connections,value=false)

View file

@ -21,4 +21,7 @@ spi.hostname.default.frontend-url = ${keycloak.frontendUrl:}
# Truststore Provider
spi.truststore.file.file=${kc.home.dir}/conf/keycloak.truststore
spi.truststore.file.password=secret
spi.truststore.file.password=secret
# http client connection reuse settings
spi.connections-http-client.default.reuse-connections=false

View file

@ -57,7 +57,6 @@
<groupId>org.wildfly.core</groupId>
<artifactId>wildfly-controller</artifactId>
</dependency>
</dependencies>
<build>

View file

@ -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() {
}
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -45,11 +45,13 @@ import org.keycloak.admin.client.Keycloak;
import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.error.KeycloakErrorHandler;
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected;
import org.keycloak.testsuite.arquillian.annotation.EnableVault;
import org.keycloak.testsuite.client.KeycloakTestingClient;
import org.keycloak.testsuite.util.LogChecker;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.SpiProvidersSwitchingUtils;
import org.keycloak.testsuite.util.SqlUtils;
import org.keycloak.testsuite.util.SystemInfoHelper;
import org.keycloak.testsuite.util.VaultUtils;
@ -350,6 +352,19 @@ public class AuthServerTestEnricher {
}
}
public static void executeCli(String... commands) throws Exception {
OnlineManagementClient client = AuthServerTestEnricher.getManagementClient();
Administration administration = new Administration(client);
for (String c : commands) {
client.execute(c).assertSuccess();
}
administration.reload();
client.close();
}
private ContainerInfo updateWithAuthServerInfo(ContainerInfo authServerInfo) {
return updateWithAuthServerInfo(authServerInfo, 0);
}
@ -531,10 +546,22 @@ public class AuthServerTestEnricher {
TestContext testContext = new TestContext(suiteContext, event.getTestClass().getJavaClass());
testContextProducer.set(testContext);
if (!isAuthServerRemote() && !isAuthServerQuarkus() && event.getTestClass().isAnnotationPresent(EnableVault.class)) {
VaultUtils.enableVault(suiteContext, event.getTestClass().getAnnotation(EnableVault.class).providerId());
restartAuthServer();
testContext.reconnectAdminClient();
if (!isAuthServerRemote() && !isAuthServerQuarkus()) {
boolean wasUpdated = false;
if (event.getTestClass().isAnnotationPresent(EnableVault.class)) {
VaultUtils.enableVault(suiteContext, event.getTestClass().getAnnotation(EnableVault.class).providerId());
wasUpdated = true;
}
if (event.getTestClass().isAnnotationPresent(SetDefaultProvider.class)) {
SpiProvidersSwitchingUtils.addProviderDefaultValue(suiteContext, event.getTestClass().getAnnotation(SetDefaultProvider.class));
wasUpdated = true;
}
if (wasUpdated) {
restartAuthServer();
testContext.reconnectAdminClient();
}
}
}
@ -851,10 +878,23 @@ public class AuthServerTestEnricher {
removeTestRealms(testContext, adminClient);
if (!isAuthServerRemote() && event.getTestClass().isAnnotationPresent(EnableVault.class)) {
VaultUtils.disableVault(suiteContext, event.getTestClass().getAnnotation(EnableVault.class).providerId());
restartAuthServer();
testContext.reconnectAdminClient();
if (!isAuthServerRemote() && !isAuthServerQuarkus()) {
boolean wasUpdated = false;
if (event.getTestClass().isAnnotationPresent(EnableVault.class)) {
VaultUtils.disableVault(suiteContext, event.getTestClass().getAnnotation(EnableVault.class).providerId());
wasUpdated = true;
}
if (event.getTestClass().isAnnotationPresent(SetDefaultProvider.class)) {
SpiProvidersSwitchingUtils.removeProvider(suiteContext, event.getTestClass().getAnnotation(SetDefaultProvider.class));
wasUpdated = true;
}
if (wasUpdated) {
restartAuthServer();
testContext.reconnectAdminClient();
}
}
if (adminClient != null) {

View file

@ -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();
}

View file

@ -42,6 +42,11 @@ public class RealmAttributeUpdater extends ServerResourceUpdater<RealmAttributeU
rep.setDefaultDefaultClientScopes(defaultClientScopes);
return this;
}
public RealmAttributeUpdater setAccessCodeLifespan(Integer accessCodeLifespan) {
rep.setAccessCodeLifespan(accessCodeLifespan);
return this;
}
public RealmAttributeUpdater setSsoSessionIdleTimeout(Integer timeout) {
rep.setSsoSessionIdleTimeout(timeout);

View file

@ -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;
}
}

View file

@ -31,13 +31,21 @@ import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.jboss.logging.Logger;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.protocol.saml.SamlProtocolUtils;
import org.keycloak.protocol.saml.profile.util.Soap;
import org.keycloak.rotation.KeyLocator;
import org.keycloak.saml.BaseSAML2BindingBuilder;
import org.keycloak.saml.SAMLRequestParser;
import org.keycloak.saml.SignatureAlgorithm;
@ -47,14 +55,27 @@ import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.saml.v2.util.DocumentUtil;
import org.keycloak.saml.processing.core.util.JAXPValidationUtil;
import org.keycloak.saml.processing.web.util.RedirectBindingUtil;
import org.keycloak.testsuite.util.saml.StepWithCheckers;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPHeaderElement;
import javax.xml.soap.SOAPMessage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
@ -68,29 +89,15 @@ import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPHeaderElement;
import javax.xml.soap.SOAPMessage;
import java.util.concurrent.TimeUnit;
import org.jboss.logging.Logger;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import org.keycloak.common.VerificationException;
import org.keycloak.protocol.saml.SamlProtocolUtils;
import org.keycloak.rotation.KeyLocator;
import static org.keycloak.saml.common.constants.GeneralConstants.RELAY_STATE;
import org.keycloak.saml.processing.web.util.RedirectBindingUtil;
import org.w3c.dom.Node;
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
import static org.keycloak.testsuite.util.SamlUtils.getSamlDeploymentForClient;
/**
* @author hmlnarik
@ -446,7 +453,7 @@ public class SamlClient {
envelope.addNamespaceDeclaration(NS_PREFIX_PAOS_BINDING, JBossSAMLURIConstants.PAOS_BINDING.get());
envelope.addNamespaceDeclaration(NS_PREFIX_PROFILE_ECP, JBossSAMLURIConstants.ECP_PROFILE.get());
SamlDeployment deployment = SamlUtils.getSamlDeploymentForClient("ecp-sp"); // TODO: Make more general for any client, currently SOAP is usable only with http://localhost:8280/ecp-sp/ client
SamlDeployment deployment = getSamlDeploymentForClient("ecp-sp"); // TODO: Make more general for any client, currently SOAP is usable only with http://localhost:8280/ecp-sp/ client
createPaosRequestHeader(envelope, deployment);
createEcpRequestHeader(envelope, deployment);
@ -490,8 +497,88 @@ public class SamlClient {
public String extractRelayState(CloseableHttpResponse response) throws IOException {
return null;
}
}
;
},
ARTIFACT_RESPONSE {
private Document extractSoapMessage(CloseableHttpResponse response) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(EntityUtils.toByteArray(response.getEntity()));
Document soapBody = Soap.extractSoapMessage(bais);
response.close();
return soapBody;
}
@Override
public SAMLDocumentHolder extractResponse(CloseableHttpResponse response, String realmPublicKey) throws IOException {
assertThat(response, statusCodeIsHC(Response.Status.OK));
Document soapBodyContents = extractSoapMessage(response);
SAMLDocumentHolder samlDoc = null;
try {
samlDoc = SAML2Request.getSAML2ObjectFromDocument(soapBodyContents);
} catch (ProcessingException | ParsingException e) {
throw new RuntimeException("Unable to get documentHolder from soapBodyResponse: " + DocumentUtil.asString(soapBodyContents));
}
if (!(samlDoc.getSamlObject() instanceof ArtifactResponseType)) {
throw new RuntimeException("Message received from ArtifactResolveService is not an ArtifactResponseMessage");
}
ArtifactResponseType art = (ArtifactResponseType) samlDoc.getSamlObject();
try {
Object artifactResponseContent = art.getAny();
if (artifactResponseContent instanceof ResponseType) {
Document doc = SAML2Request.convert((ResponseType) artifactResponseContent);
return new SAMLDocumentHolder((ResponseType) artifactResponseContent, doc);
} else if (artifactResponseContent instanceof RequestAbstractType) {
Document doc = SAML2Request.convert((RequestAbstractType) art.getAny());
return new SAMLDocumentHolder((RequestAbstractType) artifactResponseContent, doc);
} else {
throw new RuntimeException("Can not recognise message contained in ArtifactResponse");
}
} catch (ParsingException | ConfigurationException | ProcessingException e) {
throw new RuntimeException("Can not obtain document from artifact response: " + DocumentUtil.asString(soapBodyContents));
}
}
@Override
public HttpUriRequest createSamlUnsignedRequest(URI samlEndpoint, String relayState, Document samlRequest) {
return null;
}
@Override
public HttpUriRequest createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey) {
return null;
}
@Override
public HttpUriRequest createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey, String certificateStr) {
return null;
}
@Override
public URI getBindingUri() {
return JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri();
}
@Override
public HttpUriRequest createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest) {
return null;
}
@Override
public HttpUriRequest createSamlSignedResponse(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey) {
return null;
}
@Override
public HttpUriRequest createSamlSignedResponse(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey, String certificateStr) {
return null;
}
@Override
public String extractRelayState(CloseableHttpResponse response) throws IOException {
return null;
}
};
public abstract SAMLDocumentHolder extractResponse(CloseableHttpResponse response, String realmPublicKey) throws IOException;
@ -688,8 +775,19 @@ public class SamlClient {
}
LOG.infof("Executing HTTP request to %s", request.getURI());
if (s instanceof StepWithCheckers) {
Runnable beforeChecker = ((StepWithCheckers) s).getBeforeStepChecker();
if (beforeChecker != null) beforeChecker.run();
}
currentResponse = client.execute(request, context);
if (s instanceof StepWithCheckers) {
Runnable afterChecker = ((StepWithCheckers) s).getAfterStepChecker();
if (afterChecker != null) afterChecker.run();
}
currentUri = request.getURI();
List<URI> locations = context.getRedirectLocations();
if (locations != null && ! locations.isEmpty()) {
@ -716,6 +814,8 @@ public class SamlClient {
}
protected HttpClientBuilder createHttpClientBuilderInstance() {
return HttpClientBuilder.create();
return HttpClientBuilder
.create()
.evictIdleConnections(100, TimeUnit.MILLISECONDS);
}
}

View file

@ -18,6 +18,7 @@ package org.keycloak.testsuite.util;
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.testsuite.client.KeycloakTestingClient;
import org.keycloak.testsuite.page.AbstractPage;
import org.keycloak.testsuite.util.SamlClient.Binding;
import org.keycloak.testsuite.util.SamlClient.DoNotFollowRedirectStep;
@ -29,8 +30,10 @@ import java.util.List;
import java.util.function.Consumer;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.keycloak.testsuite.util.saml.CreateArtifactMessageStepBuilder;
import org.keycloak.testsuite.util.saml.CreateAuthnRequestStepBuilder;
import org.keycloak.testsuite.util.saml.CreateLogoutRequestStepBuilder;
import org.keycloak.testsuite.util.saml.HandleArtifactStepBuilder;
import org.keycloak.testsuite.util.saml.IdPInitiatedLoginBuilder;
import org.keycloak.testsuite.util.saml.LoginBuilder;
import org.keycloak.testsuite.util.saml.UpdateProfileBuilder;
@ -105,7 +108,6 @@ public class SamlClientBuilder {
/**
* Adds a single generic step
* @param step
* @return This builder
*/
public SamlClientBuilder addStep(Runnable stepWithNoParameters) {
@ -259,4 +261,20 @@ public class SamlClientBuilder {
});
}
public HandleArtifactStepBuilder handleArtifact(URI authServerSamlUrl, String issuer) {
return doNotFollowRedirects()
.addStepBuilder(new HandleArtifactStepBuilder(authServerSamlUrl, issuer, this));
}
public HandleArtifactStepBuilder handleArtifact(HandleArtifactStepBuilder handleArtifactStepBuilder) {
return doNotFollowRedirects().addStepBuilder(handleArtifactStepBuilder);
}
public CreateArtifactMessageStepBuilder artifactMessage(URI authServerSamlUrl, String issuer, Binding requestBinding) {
return addStepBuilder(new CreateArtifactMessageStepBuilder(authServerSamlUrl, issuer, requestBinding,this));
}
public CreateArtifactMessageStepBuilder artifactMessage(CreateArtifactMessageStepBuilder camb) {
return addStepBuilder(camb);
}
}

View file

@ -1,9 +1,16 @@
package org.keycloak.testsuite.util;
import org.apache.tools.ant.filters.StringInputStream;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
import org.keycloak.adapters.saml.config.parsers.ResourceLoader;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType;
import org.keycloak.dom.saml.v2.metadata.SPSSODescriptorType;
import org.keycloak.protocol.saml.installation.SamlSPDescriptorClientInstallation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
import org.keycloak.testsuite.utils.arquillian.DeploymentArchiveProcessorUtils;
import org.keycloak.testsuite.utils.io.IOUtil;
import org.w3c.dom.Document;
@ -32,4 +39,16 @@ public class SamlUtils {
};
return new DeploymentBuilder().build(isProcessed, loader);
}
public static SPSSODescriptorType getSPInstallationDescriptor(ClientsResource res, String clientId) throws ParsingException {
String spDescriptorString = res.findByClientId(clientId).stream().findFirst()
.map(ClientRepresentation::getId)
.map(res::get)
.map(clientResource -> clientResource.getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR))
.orElseThrow(() -> new RuntimeException("Missing descriptor"));
SAMLParser parser = SAMLParser.getInstance();
EntityDescriptorType o = (EntityDescriptorType) parser.parse(new StringInputStream(spDescriptorString));
return o.getChoiceType().get(0).getDescriptors().get(0).getSpDescriptor();
}
}

View file

@ -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();
}
}
}

View file

@ -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;
}
}

View file

@ -49,6 +49,7 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
private String signingPublicKeyPem; // TODO: should not be needed
private String signingPrivateKeyPem;
private String signingCertificate;
private URI protocolBinding;
private String authorizationHeader;
private final Document forceLoginRequestDocument;
@ -86,6 +87,15 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
return this;
}
public CreateAuthnRequestStepBuilder setProtocolBinding(URI protocolBinding) {
this.protocolBinding = protocolBinding;
return this;
}
public URI getProtocolBinding() {
return protocolBinding;
}
public CreateAuthnRequestStepBuilder signWith(String signingPrivateKeyPem, String signingPublicKeyPem) {
return signWith(signingPrivateKeyPem, signingPublicKeyPem, null);
}
@ -96,7 +106,7 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
this.signingCertificate = signingCertificate;
return this;
}
public CreateAuthnRequestStepBuilder basicAuthentication(UserRepresentation user) {
String username = user.getUsername();
String password = Users.getPasswordOf(user);
@ -126,7 +136,7 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
if (authorizationHeader != null) {
request.addHeader(HttpHeaders.AUTHORIZATION, authorizationHeader);
}
return request;
}
@ -137,9 +147,10 @@ public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder<Authn
try {
SAML2Request samlReq = new SAML2Request();
AuthnRequestType loginReq = samlReq.createAuthnRequestType(UUID.randomUUID().toString(),
assertionConsumerURL, this.authServerSamlUrl.toString(), issuer, requestBinding.getBindingUri());
AuthnRequestType loginReq = samlReq.createAuthnRequestType(UUID.randomUUID().toString(), assertionConsumerURL, this.authServerSamlUrl.toString(), issuer, requestBinding.getBindingUri());
if (protocolBinding != null) {
loginReq.setProtocolBinding(protocolBinding);
}
return SAML2Request.convert(loginReq);
} catch (ConfigurationException | ParsingException | ProcessingException ex) {
throw new RuntimeException(ex);

View file

@ -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;
}
}

View file

@ -16,6 +16,8 @@
*/
package org.keycloak.testsuite.util.saml;
import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.testsuite.util.SamlClientBuilder;
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.saml.common.constants.GeneralConstants;
@ -81,6 +83,9 @@ public class ModifySamlResponseStepBuilder extends SamlDocumentStepBuilder<SAML2
case POST:
return handlePostBinding(currentResponse);
case ARTIFACT_RESPONSE:
return handleArtifactResponse(currentResponse);
}
throw new RuntimeException("Unknown binding for " + ModifySamlResponseStepBuilder.class.getName());
@ -130,6 +135,18 @@ public class ModifySamlResponseStepBuilder extends SamlDocumentStepBuilder<SAML2
return this;
}
private HttpUriRequest handleArtifactResponse(CloseableHttpResponse currentResponse) throws Exception {
SAMLDocumentHolder samlDocumentHolder = null;
try {
samlDocumentHolder = Binding.ARTIFACT_RESPONSE.extractResponse(currentResponse);
} catch (IOException e) {
e.printStackTrace();
}
return createRequest(this.targetUri, this.targetAttribute, DocumentUtil.asString(samlDocumentHolder.getSamlDocument()), new LinkedList<>());
}
protected HttpUriRequest handleRedirectBinding(CloseableHttpResponse currentResponse) throws Exception, IOException, URISyntaxException {
String samlDoc;
final String attrName;

View file

@ -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();
}
}
}

View file

@ -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)));
}
});
}
}

View file

@ -0,0 +1,10 @@
package org.keycloak.testsuite.util.saml;
public interface StepWithCheckers {
default Runnable getBeforeStepChecker() {
return null;
}
default Runnable getAfterStepChecker() {
return null;
}
}

View file

@ -191,13 +191,13 @@ public abstract class AbstractKeycloakTest {
protected void beforeAbstractKeycloakTestRealmImport() throws Exception {
}
protected void postAfterAbstractKeycloak() {
protected void postAfterAbstractKeycloak() throws Exception {
}
protected void afterAbstractKeycloakTestRealmImport() {}
@After
public void afterAbstractKeycloakTest() {
public void afterAbstractKeycloakTest() throws Exception {
if (resetTimeOffset) {
resetTimeOffset();
}

View file

@ -582,8 +582,9 @@ public class RealmTest extends AbstractAdminTest {
ClientRepresentation converted = realm.convertClientDescription(description);
assertEquals("loadbalancer-9.siroe.com", converted.getClientId());
assertEquals(1, converted.getRedirectUris().size());
assertEquals(2, converted.getRedirectUris().size());
assertEquals("https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp", converted.getRedirectUris().get(0));
assertEquals("https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp", converted.getRedirectUris().get(1));
}
public static void assertRealm(RealmRepresentation realm, RealmRepresentation storedRealm) {

View file

@ -80,8 +80,9 @@ public class SAMLClientRegistrationTest extends AbstractClientRegistrationTest {
"https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/post",
"https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/soap",
"https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/paos",
"https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/redirect"
)); // No redirect URI for ARTIFACT binding which is unsupported
"https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/redirect",
"https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp/artifact"
));
assertThat(response.getAttributes().get("saml_single_logout_service_url_redirect"), is("https://LoadBalancer-9.siroe.com:3443/federation/SPSloRedirect/metaAlias/sp"));

View file

@ -67,7 +67,7 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest
@After
@Override
public void afterAbstractKeycloakTest() {
public void afterAbstractKeycloakTest() throws Exception {
log.debug("--DC: after AbstractCrossDCTest");
CrossDCTestEnricher.startAuthServerBackendNode(DC.FIRST, 0); // make sure first node is started
enableOnlyFirstNodeInFirstDc();

View file

@ -128,7 +128,7 @@ public class DockerClientTest extends AbstractKeycloakTest {
}
@Override
public void afterAbstractKeycloakTest() {
public void afterAbstractKeycloakTest() throws Exception {
super.afterAbstractKeycloakTest();
pause(5000); // wait for the container logs

View file

@ -162,7 +162,7 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
@After
@Override
public void afterAbstractKeycloakTest() {
public void afterAbstractKeycloakTest() throws Exception {
cleanupApacheHttpClient();
super.afterAbstractKeycloakTest();

View file

@ -80,7 +80,7 @@ public class OAuthRedirectUriTest extends AbstractKeycloakTest {
}
@Override
public void afterAbstractKeycloakTest() {
public void afterAbstractKeycloakTest() throws Exception {
super.afterAbstractKeycloakTest();
server.stop(0);

View file

@ -8,13 +8,13 @@ import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.AbstractAuthTest;
import org.keycloak.testsuite.util.SamlClient;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriBuilderException;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import java.net.URI;
import java.security.KeyFactory;
import java.security.PrivateKey;
@ -52,9 +52,13 @@ public abstract class AbstractSamlTest extends AbstractAuthTest {
public static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST2 = AUTH_SERVER_SCHEME + "://localhost:" + (AUTH_SERVER_SSL_REQUIRED ? AUTH_SERVER_PORT : 8080) + "/sales-post2/saml";
public static final String SAML_CLIENT_ID_SALES_POST2 = "http://localhost:8280/sales-post2/";
public static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST_SIG = AUTH_SERVER_SCHEME + "://localhost:" + (AUTH_SERVER_SSL_REQUIRED ? AUTH_SERVER_PORT : 8080) + "/sales-post-sig/";
public static final String SAML_CLIENT_ID_SALES_POST_SIG = "http://localhost:8280/sales-post-sig/";
public static final String SAML_URL_SALES_POST_SIG = "http://localhost:8080/sales-post-sig/";
public static final String SAML_CLIENT_ID_SALES_POST_SIG = "http://localhost:8280/sales-post-sig/";
public static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST_SIG = AUTH_SERVER_SCHEME + "://localhost:" + (AUTH_SERVER_SSL_REQUIRED ? AUTH_SERVER_PORT : 8080) + "/sales-post-sig/";
public static final String SAML_CLIENT_ID_SALES_POST_ASSERTION_AND_RESPONSE_SIG = "http://localhost:8280/sales-post-assertion-and-response-sig/";
public static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST_ASSERTION_AND_RESPONSE_SIG = AUTH_SERVER_SCHEME + "://localhost:" + (AUTH_SERVER_SSL_REQUIRED ? AUTH_SERVER_PORT : 8080) + "/sales-post-assertion-and-response-sig/";
public static final String SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBANUbxrvEY3pkiQNt55zJLKBwN+zKmNQw08ThAmOKzwHfXoK+xlDSFxNMtTKJGkeUdnKzaTfESEcEfKYULUA41y/NnOlvjS0CEsc7Wq0Ce63TSSGMB2NHea4tV0aQz/MwLsbmz2IjAFWHA5CHL5WwacIf3UTOSNnhJUSvnkomjJAlAgMBAAECgYANpO2gb/5+g5lSIuNFYov86bJq8r2+ODIW1OE2Rljioc6HSHeiDRF1JuAjECwikRrUVTBTZbnK8jqY14neJsWAKBzGo+ToaQALsNZ9B91DxxL50K5oVOzw5shAS9TnRjN40+KIXFED4ydq4JRdoqb8+cN+N3i0+Cu7tdm+UaHTAQJBAOwFs3ZwqQEqmv9vmgmIFwFpJm1aIw25gEOf3Hy45GP4bL/j0FQgwcXYRbLE5bPqhw/liLKc1GQ97bVm6zs8SvUCQQDnJZA6TFRMiDjezinE1J4e0v4RupyDniVjbE5ArTK5/FRVkjw4Ny0AqZUEyIIqlTeZlCq45pCJy4a2hymDGVJxAj9gzfXNnmezEsZ//kYvoqHM8lPQhifaeTsigW7tuOf0GPCBw+6uksDnZM0xhZCxOoArBPoMSEbU1pGo1Y2lvhUCQF6E5sBgHAybm53Ich4Rz4LNRqWbSIstrR5F2I3sBRU2kInZXZSjQ1zE+7HUCB4/nFfJ1dp8NdiTCEg1Zw072pECQQDnxyQALmWhQbBTl0tq6CwYf9rZDwBzxuY+CXB8Ky1gOmXwan96KZvV4rK8MQQs6HIiYC/j+5lX3A3zlXTFldaz";
public static final String SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVG8a7xGN6ZIkDbeecySygcDfsypjUMNPE4QJjis8B316CvsZQ0hcTTLUyiRpHlHZys2k3xEhHBHymFC1AONcvzZzpb40tAhLHO1qtAnut00khjAdjR3muLVdGkM/zMC7G5s9iIwBVhwOQhy+VsGnCH91EzkjZ4SVEr55KJoyQJQIDAQAB";
public static final PrivateKey SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY_PK;
@ -76,7 +80,7 @@ public abstract class AbstractSamlTest extends AbstractAuthTest {
public static final String SAML_CLIENT_SALES_POST_SIG_EXPIRED_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDKxs0adx1X+k4u+a5eZjwD17mvADwgiwDYpMznfNlSNEfDJdFAHIZH0VAbwXnaGySJ/a/MMMTHly5irDMp1udkmHgv2ceW+SumsjEtxliSIKi6af59aYlHiOLGyV5VI/VLVvkE6Roax7fZ+7O858KDahg1JI5smYnpBLKY3X885QIDAQAB";
public static final String SAML_CLIENT_SALES_POST_SIG_EXPIRED_CERTIFICATE = "MIICMTCCAZqgAwIBAgIJAPlizW20Nhe6MA0GCSqGSIb3DQEBCwUAMDAxLjAsBgNVBAMMJWh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9zYWxlcy1wb3N0LXNpZy8wHhcNMTYwODI5MDg1MjMzWhcNMTYwODMwMDg1MjMzWjAwMS4wLAYDVQQDDCVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDKxs0adx1X+k4u+a5eZjwD17mvADwgiwDYpMznfNlSNEfDJdFAHIZH0VAbwXnaGySJ/a/MMMTHly5irDMp1udkmHgv2ceW+SumsjEtxliSIKi6af59aYlHiOLGyV5VI/VLVvkE6Roax7fZ+7O858KDahg1JI5smYnpBLKY3X885QIDAQABo1MwUTAdBgNVHQ4EFgQUE9C6Ck0jsdY+sjN064ZYwYkZJr4wHwYDVR0jBBgwFoAUE9C6Ck0jsdY+sjN064ZYwYkZJr4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQBuypHw5DMDBgfI6LcXBiCjpiQP3DLRLdwthh/RfCnZT7PrhXRJV8RMm8EqxqtEgfg2SKqMyA02uxMKH0p277U2iQveSDAaICTJRxtyFm6FERtgLNlsekusC2I14gZpLe84oHDf6L1w3dKFzzLEC9+bHg/XCg/KthWxW8iuVct5qg==";
public static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST_ENC = "http://localhost:8080/sales-post-enc/";
public static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST_ENC = AUTH_SERVER_SCHEME + "://localhost:" + (AUTH_SERVER_SSL_REQUIRED ? AUTH_SERVER_PORT : 8080) + "/sales-post-enc/saml";
public static final String SAML_CLIENT_ID_SALES_POST_ENC = "http://localhost:8280/sales-post-enc/";
public static final String SAML_CLIENT_SALES_POST_ENC_PRIVATE_KEY = "MIICXQIBAAKBgQDb7kwJPkGdU34hicplwfp6/WmNcaLh94TSc7Jyr9Undp5pkyLgb0DE7EIE+6kSs4LsqCb8HDkB0nLD5DXbBJFd8n0WGoKstelvtg6FtVJMnwN7k7yZbfkPECWH9zF70VeOo9vbzrApNRnct8ZhH5fbflRB4JMA9L9R+LbURdoSKQIDAQABAoGBANtbZG9bruoSGp2s5zhzLzd4hczT6Jfk3o9hYjzNb5Z60ymN3Z1omXtQAdEiiNHkRdNxK+EM7TcKBfmoJqcaeTkW8cksVEAW23ip8W9/XsLqmbU2mRrJiKa+KQNDSHqJi1VGyimi4DDApcaqRZcaKDFXg2KDr/Qt5JFD/o9IIIPZAkEA+ZENdBIlpbUfkJh6Ln+bUTss/FZ1FsrcPZWu13rChRMrsmXsfzu9kZUWdUeQ2Dj5AoW2Q7L/cqdGXS7Mm5XhcwJBAOGZq9axJY5YhKrsksvYRLhQbStmGu5LG75suF+rc/44sFq+aQM7+oeRr4VY88Mvz7mk4esdfnk7ae+cCazqJvMCQQCx1L1cZw3yfRSn6S6u8XjQMjWE/WpjulujeoRiwPPY9WcesOgLZZtYIH8nRL6ehEJTnMnahbLmlPFbttxPRUanAkA11MtSIVcKzkhp2KV2ipZrPJWwI18NuVJXb+3WtjypTrGWFZVNNkSjkLnHIeCYlJIGhDd8OL9zAiBXEm6kmgLNAkBWAg0tK2hCjvzsaA505gWQb4X56uKWdb0IzN+fOLB3Qt7+fLqbVQNQoNGzqey6B4MoS1fUKAStqdGTFYPG/+9t";
public static final String SAML_CLIENT_SALES_POST_ENC_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDb7kwJPkGdU34hicplwfp6/WmNcaLh94TSc7Jyr9Undp5pkyLgb0DE7EIE+6kSs4LsqCb8HDkB0nLD5DXbBJFd8n0WGoKstelvtg6FtVJMnwN7k7yZbfkPECWH9zF70VeOo9vbzrApNRnct8ZhH5fbflRB4JMA9L9R+LbURdoSKQIDAQAB";

View file

@ -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")));
}
}

View file

@ -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"))));
}
}

View file

@ -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());
}
}

View file

@ -128,7 +128,7 @@
"connectionsHttpClient": {
"default": {
"max-connection-idle-time-millis": 100
"reuse-connections": false
}
},
@ -235,5 +235,9 @@
"dir": "target/dependency/vault",
"enabled": "${keycloak.vault.files-plaintext.provider.enabled:false}"
}
},
"saml-artifact-resolver": {
"provider": "${keycloak.saml-artifact-resolver.provider:default}"
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -148,7 +148,7 @@ public class ClientSettingsForm extends CreateClientForm {
public void setEnabled(boolean enabled) {
enabledSwitch.setOn(enabled);
}
public boolean isAlwaysDisplayInConsole() {
return alwaysDisplayInConsole.isOn();
}
@ -335,4 +335,4 @@ public class ClientSettingsForm extends CreateClientForm {
}
}
}
}

View file

@ -50,7 +50,7 @@ public class ClientSettingsTest extends AbstractClientTest {
private ClientSettings clientSettingsPage;
private ClientRepresentation newClient;
@Test
public void crudOIDCPublic() {
newClient = createClientRep("oidc-public", OIDC);
@ -60,22 +60,22 @@ public class ClientSettingsTest extends AbstractClientTest {
ClientRepresentation found = findClientByClientId(newClient.getClientId());
assertNotNull("Client " + newClient.getClientId() + " was not found.", found);
assertClientSettingsEqual(newClient, found);
// update & verify
newClient.setClientId("oidc-public-updated");
newClient.setName("updatedName");
List<String> redirectUris = new ArrayList<>();
redirectUris.add("http://example2.test/app/*");
redirectUris.add("http://example2.test/app2/*");
redirectUris.add("http://example3.test/app/*");
newClient.setRedirectUris(redirectUris);
List<String> webOrigins = new ArrayList<>();
webOrigins.add("http://example2.test");
webOrigins.add("http://example3.test");
newClient.setWebOrigins(webOrigins);
clientSettingsPage.form().setClientId("oidc-public-updated");
clientSettingsPage.form().setName("updatedName");
clientSettingsPage.form().setRedirectUris(redirectUris);
@ -84,7 +84,7 @@ public class ClientSettingsTest extends AbstractClientTest {
assertAlertSuccess();
assertFalse(clientSettingsPage.form().isAlwaysDisplayInConsoleVisible());
found = findClientByClientId(newClient.getClientId());
assertNotNull("Client " + newClient.getClientId() + " was not found.", found);
assertClientSettingsEqual(newClient, found);
@ -130,10 +130,10 @@ public class ClientSettingsTest extends AbstractClientTest {
public void createOIDCConfidential() {
newClient = createClientRep("oidc-confidetial", OIDC);
createClient(newClient);
newClient.setRedirectUris(TEST_REDIRECT_URIs);
newClient.setPublicClient(false);
clientSettingsPage.form().setAccessType(CONFIDENTIAL);
clientSettingsPage.form().setRedirectUris(TEST_REDIRECT_URIs);
clientSettingsPage.form().save();
@ -142,29 +142,29 @@ public class ClientSettingsTest extends AbstractClientTest {
assertNotNull("Client " + newClient.getClientId() + " was not found.", found);
assertClientSettingsEqual(newClient, found);
}
//KEYCLOAK-4022
@Test
public void testOIDCConfidentialServiceAccountRolesTab() {
newClient = createClientRep("oidc-service-account-tab", OIDC);
createClient(newClient);
newClient.setRedirectUris(TEST_REDIRECT_URIs);
newClient.setPublicClient(false);
clientSettingsPage.form().setAccessType(CONFIDENTIAL);
clientSettingsPage.form().setServiceAccountsEnabled(true);
assertTrue(clientSettingsPage.form().isServiceAccountsEnabled());
//check if Service Account Roles tab is not present
assertFalse(clientSettingsPage.tabs().isServiceAccountRolesDisplayed());
clientSettingsPage.form().setRedirectUris(TEST_REDIRECT_URIs);
clientSettingsPage.form().save();
//should be there now
assertTrue(clientSettingsPage.tabs().getTabs().findElement(By.linkText("Service Account Roles")).isDisplayed());
}
@Test
public void saveOIDCConfidentialWithoutRedirectURIs() {
newClient = createClientRep("oidc-confidential", OIDC);
@ -182,10 +182,10 @@ public class ClientSettingsTest extends AbstractClientTest {
clientSettingsPage.form().setAccessType(BEARER_ONLY);
clientSettingsPage.form().save();
newClient.setBearerOnly(true);
newClient.setPublicClient(false);
ClientRepresentation found = findClientByClientId(newClient.getClientId());
assertNotNull("Client " + newClient.getClientId() + " was not found.", found);
assertClientSettingsEqual(newClient, found);
@ -201,7 +201,7 @@ public class ClientSettingsTest extends AbstractClientTest {
assertClientSettingsEqual(newClient, found);
assertClientSamlAttributes(getSAMLAttributes(), found.getAttributes());
}
@Test
public void invalidSettings() {
clientsPage.table().createClient();

View file

@ -335,6 +335,8 @@ include-authnstatement=Include AuthnStatement
include-authnstatement.tooltip=Should a statement specifying the method and timestamp be included in login responses?
include-onetimeuse-condition=Include OneTimeUse Condition
include-onetimeuse-condition.tooltip=Should a OneTimeUse Condition be included in login responses?
artifact-binding = Force Artifact Binding
artifact-binding.tooltip = Should response messages be returned to the client through the SAML ARTIFACT binding system?
sign-documents=Sign Documents
sign-documents.tooltip=Should SAML documents be signed by the realm?
sign-documents-redirect-enable-key-info-ext=Optimize REDIRECT signing key lookup
@ -407,6 +409,12 @@ logout-service-post-binding-url=Logout Service POST Binding URL
logout-service-post-binding-url.tooltip=SAML POST Binding URL for the client's single logout service. You can leave this blank if you are using a different binding
logout-service-redir-binding-url=Logout Service Redirect Binding URL
logout-service-redir-binding-url.tooltip=SAML Redirect Binding URL for the client's single logout service. You can leave this blank if you are using a different binding.
logout-service-artifact-binding-url=Logout Service ARTIFACT Binding URL
logout-service-artifact-binding-url.tooltip=SAML ARTIFACT Binding URL for the client's single logout service. You can leave this blank if you are using a different binding.
artifact-binding-url= Artifact Binding URL
artifact-binding-url.tooltip=URL to send the HTTP ARTIFACT messages to. You can leave this blank if you are using a different binding. This value should be set when forcing ARTIFACT binding together with IdP initiated login.
artifact-resolution-service-url= Artifact Resolution Service
artifact-resolution-service-url.tooltip= SAML Artifact resolution service for the client. This is the endpoint to which Keycloak will send a SOAP ArtifactResolve mesasge. You can leave this blank if you do not have a URL for this binding.
saml-signature-keyName-transformer=SAML Signature Key Name
saml-signature-keyName-transformer.tooltip=Signed SAML documents contain identification of signing key in KeyName element. For Keycloak / RH-SSO counterparty, use KEY_ID, for MS AD FS use CERT_SUBJECT, for others check and use NONE if no other option works.
oidc-compatibility-modes=OpenID Connect Compatibility Modes

View file

@ -1097,6 +1097,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
$scope.samlAuthnStatement = false;
$scope.samlOneTimeUseCondition = false;
$scope.samlMultiValuedRoles = false;
$scope.samlArtifactBinding = false;
$scope.samlServerSignature = false;
$scope.samlServerSignatureEnableKeyInfoExtension = false;
$scope.samlAssertionSignature = false;
@ -1179,6 +1180,16 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
} else if ($scope.client.attributes['saml_name_id_format'] == 'persistent') {
$scope.nameIdFormat = $scope.nameIdFormats[3];
}
if ($scope.client.attributes["saml.artifact.binding"]) {
if ($scope.client.attributes["saml.artifact.binding"] == "true") {
$scope.samlArtifactBinding = true;
} else {
$scope.samlArtifactBinding = false;
}
}
if ($scope.client.attributes["saml.server.signature"]) {
if ($scope.client.attributes["saml.server.signature"] == "true") {
$scope.samlServerSignature = true;
@ -1635,6 +1646,11 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
}
delete $scope.clientEdit.requestUris;
if ($scope.samlArtifactBinding == true) {
$scope.clientEdit.attributes["saml.artifact.binding"] = "true";
} else {
$scope.clientEdit.attributes["saml.artifact.binding"] = "false";
}
if ($scope.samlServerSignature == true) {
$scope.clientEdit.attributes["saml.server.signature"] = "true";
} else {

View file

@ -171,6 +171,15 @@
</div>
<kc-tooltip>{{:: 'include-onetimeuse-condition.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-md-2 control-label" for="samlArtifactBinding">{{:: 'artifact-binding' | translate}}</label>
<div class="col-sm-6">
<input ng-model="samlArtifactBinding" ng-click="switchChange()" name="samlArtifactBinding" id="samlArtifactBinding" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
<kc-tooltip>{{:: 'artifact-binding.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-md-2 control-label" for="samlServerSignature">{{:: 'sign-documents' | translate}}</label>
<div class="col-sm-6">
@ -416,12 +425,35 @@
<kc-tooltip>{{:: 'logout-service-post-binding-url.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-md-2 control-label" for="logoutPostBinding">{{:: 'logout-service-redir-binding-url' | translate}}</label>
<label class="col-md-2 control-label" for="logoutRedirectBinding">{{:: 'logout-service-redir-binding-url' | translate}}</label>
<div class="col-sm-6">
<input ng-model="clientEdit.attributes.saml_single_logout_service_url_redirect" class="form-control" type="text" name="logoutRedirectBinding" id="logoutRedirectBinding" />
</div>
<kc-tooltip>{{:: 'logout-service-redir-binding-url.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-md-2 control-label" for="logoutArtifactBinding">{{:: 'logout-service-artifact-binding-url' | translate}}</label>
<div class="col-sm-6">
<input ng-model="clientEdit.attributes.saml_single_logout_service_url_artifact" class="form-control" type="text" name="logoutRedirectBinding" id="logoutArtifactBinding" />
</div>
<kc-tooltip>{{:: 'logout-service-artifact-binding-url.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-md-2 control-label" for="artifactBindingUrl">{{:: 'artifact-binding-url' | translate}}</label>
<div class="col-sm-6">
<input ng-model="clientEdit.attributes.saml_artifact_binding_url" class="form-control" type="text" name="artifactBindingUrl" id="artifactBindingUrl" />
</div>
<kc-tooltip>{{:: 'artifact-binding-url.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-md-2 control-label" for="artifactResolutionServiceUrl">{{:: 'artifact-resolution-service-url' | translate}}</label>
<div class="col-sm-6">
<input ng-model="clientEdit.attributes.saml_artifact_resolution_service_url" class="form-control" type="text" name="artifactResolutionServiceUrl" id="artifactResolutionServiceUrl" />
</div>
<kc-tooltip>{{:: 'artifact-resolution-service-url.tooltip' | translate}}</kc-tooltip>
</div>
</fieldset>
<fieldset data-ng-show="protocol == 'openid-connect'">

View file

@ -367,6 +367,7 @@ openshift.scope.list-projects=List projects
saml.post-form.title=Authentication Redirect
saml.post-form.message=Redirecting, please wait.
saml.post-form.js-disabled=JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue.
saml.artifactResolutionServiceInvalidResponse=Unable to resolve artifact.
#authenticators
otp-display-name=Authenticator Application