diff --git a/docs/documentation/server_admin/topics/identity-broker/saml.adoc b/docs/documentation/server_admin/topics/identity-broker/saml.adoc
index 33915d384e6..e4b4c9fa09e 100644
--- a/docs/documentation/server_admin/topics/identity-broker/saml.adoc
+++ b/docs/documentation/server_admin/topics/identity-broker/saml.adoc
@@ -25,6 +25,9 @@ image:images/saml-add-identity-provider.png[Add Identity Provider]
|Single Sign-On Service URL
|The SAML endpoint that starts the authentication process. If your SAML IDP publishes an IDP entity descriptor, the value of this field is specified there.
+|Artifact service URL
+|The SAML artifact resolution endpoint. If your SAML IDP publishes an IDP entity descriptor, the value of this field is specified there.
+
|Single Logout Service URL
|The SAML logout endpoint. If your SAML IDP publishes an IDP entity descriptor, the value of this field is specified there.
@@ -46,6 +49,9 @@ image:images/saml-add-identity-provider.png[Add Identity Provider]
|HTTP-POST Binding Response
|Controls the SAML binding in response to any SAML requests sent by an external IDP. When *OFF*, {project_name} uses Redirect Binding.
+|ARTIFACT Binding Response
+|Controls the SAML binding in response to any SAML requests sent by an external IDP. When *OFF*, {project_name} evaluates the HTTP-POST Binding Response configuration.
+
|HTTP-POST Binding for AuthnRequest
|Controls the SAML binding when requesting authentication from an external IDP. When *OFF*, {project_name} uses Redirect Binding.
diff --git a/js/apps/admin-ui/cypress/e2e/partial_import_test.spec.ts b/js/apps/admin-ui/cypress/e2e/partial_import_test.spec.ts
index 46247c2f081..2ad43ee0c8f 100644
--- a/js/apps/admin-ui/cypress/e2e/partial_import_test.spec.ts
+++ b/js/apps/admin-ui/cypress/e2e/partial_import_test.spec.ts
@@ -119,8 +119,8 @@ describe("Partial import test", () => {
//clear button should be disabled if there is nothing in the dialog
modal.clearButton().should("be.disabled");
- modal.textArea().type("{}", { force: true });
- modal.textArea().get(".view-lines").should("have.text", "{}");
+ modal.textArea().type("test", { force: true });
+ modal.textArea().get(".view-lines").should("have.text", "test");
modal.clearButton().should("not.be.disabled");
modal.clearButton().click();
modal.clickClearConfirmButton();
diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties
index 0b614344422..3acb98ba1c7 100644
--- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties
+++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties
@@ -376,6 +376,7 @@ createAttributeError=Error\! User Profile configuration has not been saved {{err
password=Password
eventTypes.VERIFY_EMAIL.name=Verify email
httpPostBindingResponseHelp=Indicates whether to respond to requests using HTTP-POST binding. If false, HTTP-REDIRECT binding will be used.
+artifactBindingResponseHelp=Indicates whether to respond to requests using ARTIFACT binding. If false, the HTTP-POST binding configuration will be evaluated.
mapperTypeHardcodedAttributeMapper=hardcoded-attribute-mapper
eventTypes.IMPERSONATE.description=Impersonate
forbidden_other=Forbidden, permissions needed\:
@@ -1748,6 +1749,7 @@ idTokenSignatureAlgorithm=ID token signature algorithm
displayHeaderHintHelp=A user-friendly name for the group that should be used when rendering a group of attributes in user-facing forms. Supports keys for localized values as well. For example\: ${profile.attribute.group.address}.
providerInfo=Provider info
ssoServiceUrl=Single Sign-On service URL
+artifactResolutionServiceUrl=Artifact Resolution service URL
inputHelperTextAfter=Helper text (under) the input field
appliedByClients=Applied by the following clients
createFlowHelp=You can create a top level flow within this from
@@ -2075,6 +2077,7 @@ experimental=Experimental
idTokenSignatureAlgorithmHelp=JWA algorithm used for signing ID tokens.
deleteResourceConfirm=If you delete this resource, some permissions will be affected.
httpPostBindingResponse=HTTP-POST binding response
+artifactBindingResponse=ARTIFACT binding response
tokenLifespan.inherited=Inherits from realm settings
saveEvents=Save events
issuer=Issuer
@@ -2825,6 +2828,7 @@ clientUpdaterTrustedHosts=Trusted Hosts
deleteSuccess=Attributes group deleted.
attributesDropdown=Attributes dropdown
ssoServiceUrlHelp=The Url that must be used to send authentication requests (SAML AuthnRequest).
+artifactResolutionServiceUrlHelp=The Url that must be used to get SAML assertions from artifacts (SAML ArtifactResolve).
copy=Copy
credentialData=Data
clientRolesConditionTooltip=Client roles, which will be checked during this condition evaluation. Condition evaluates to true if client has at least one client role with the name as the client roles specified in the configuration.
diff --git a/js/apps/admin-ui/src/identity-providers/add/DescriptorSettings.tsx b/js/apps/admin-ui/src/identity-providers/add/DescriptorSettings.tsx
index e4ab8951ecd..f5829aa7ee7 100644
--- a/js/apps/admin-ui/src/identity-providers/add/DescriptorSettings.tsx
+++ b/js/apps/admin-ui/src/identity-providers/add/DescriptorSettings.tsx
@@ -70,6 +70,13 @@ const Fields = ({ readOnly }: DescriptorSettingsProps) => {
readOnly={readOnly}
rules={{ required: t("required") }}
/>
+
{
stringify
/>
+
+
{
+ protected String artifact;
+ protected String destination;
+ protected NameIDType issuer;
+ protected final List extensions = new LinkedList<>();
+
+ public SAML2ArtifactResolveRequestBuilder artifact(String artifact) {
+ this.artifact = artifact;
+ return this;
+ }
+
+ public SAML2ArtifactResolveRequestBuilder destination(String destination) {
+ this.destination = destination;
+ return this;
+ }
+
+ public SAML2ArtifactResolveRequestBuilder issuer(NameIDType issuer) {
+ this.issuer = issuer;
+ return this;
+ }
+
+ public SAML2ArtifactResolveRequestBuilder issuer(String issuer) {
+ return issuer(SAML2NameIDBuilder.value(issuer).build());
+ }
+
+ @Override
+ public SAML2ArtifactResolveRequestBuilder addExtension(NodeGenerator extension) {
+ this.extensions.add(extension);
+ return this;
+ }
+
+ public Document buildDocument() throws ProcessingException, ConfigurationException, ParsingException {
+ Document document = SAML2Request.convert(createArtifactResolveRequest());
+ return document;
+ }
+
+ public ArtifactResolveType createArtifactResolveRequest() throws ConfigurationException {
+ ArtifactResolveType lort = SAML2Request.createArtifactResolveRequest(issuer);
+
+ lort.setIssuer(issuer);
+
+ if (destination != null) {
+ lort.setDestination(URI.create(destination));
+ }
+
+ if (artifact != null) {
+ lort.setArtifact(artifact);
+ }
+
+ if (!this.extensions.isEmpty()) {
+ ExtensionsType extensionsType = new ExtensionsType();
+ for (NodeGenerator extension : this.extensions) {
+ extensionsType.addExtension(extension);
+ }
+ lort.setExtensions(extensionsType);
+ }
+
+ return lort;
+ }
+}
\ No newline at end of file
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java
index 4790f62b789..c99cc6674ea 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java
@@ -18,6 +18,7 @@ package org.keycloak.saml.processing.api.saml.v2.request;
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
+import org.keycloak.dom.saml.v2.protocol.ArtifactResolveType;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.NameIDPolicyType;
@@ -275,6 +276,22 @@ public class SAML2Request {
return lrt;
}
+ /**
+ * Create a Artifact Resolve Request
+ *
+ * @param issuer
+ *
+ * @return
+ *
+ * @throws ConfigurationException
+ */
+ public static ArtifactResolveType createArtifactResolveRequest(NameIDType issuer) {
+ ArtifactResolveType lrt = new ArtifactResolveType(IDGenerator.create("ID_"), XMLTimeUtil.getIssueInstant());
+
+ lrt.setIssuer(issuer);
+
+ return lrt;
+ }
/**
* Return the DOM object
*
@@ -294,6 +311,8 @@ public class SAML2Request {
writer.write((AuthnRequestType) rat);
} else if (rat instanceof LogoutRequestType) {
writer.write((LogoutRequestType) rat);
+ } else if (rat instanceof ArtifactResolveType) {
+ writer.write((ArtifactResolveType) rat);
}
return DocumentUtil.getDocument(new String(bos.toByteArray(), GeneralConstants.SAML_CHARSET));
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/response/SAML2Response.java b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/response/SAML2Response.java
index 1500512e03b..7fc5924c195 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/response/SAML2Response.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/response/SAML2Response.java
@@ -34,6 +34,7 @@ import org.keycloak.dom.saml.v2.assertion.StatementAbstractType;
import org.keycloak.dom.saml.v2.assertion.SubjectConfirmationDataType;
import org.keycloak.dom.saml.v2.assertion.SubjectConfirmationType;
import org.keycloak.dom.saml.v2.assertion.SubjectType;
+import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import org.keycloak.saml.common.PicketLinkLogger;
@@ -445,7 +446,10 @@ public class SAML2Response {
SAMLResponseWriter writer = new SAMLResponseWriter(StaxUtil.getXMLStreamWriter(bos));
- if (responseType instanceof ResponseType) {
+ if (responseType instanceof ArtifactResponseType) {
+ ArtifactResponseType response = (ArtifactResponseType) responseType;
+ writer.write(response);
+ } else if (responseType instanceof ResponseType) {
ResponseType response = (ResponseType) responseType;
writer.write(response);
} else {
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
index 955229c4ce9..8b41ad7ad05 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
@@ -33,6 +33,7 @@ import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.assertion.SubjectConfirmationDataType;
import org.keycloak.dom.saml.v2.assertion.SubjectConfirmationType;
import org.keycloak.dom.saml.v2.assertion.SubjectType;
+import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
@@ -62,8 +63,10 @@ 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.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants;
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
@@ -118,6 +121,7 @@ import org.keycloak.saml.validators.DestinationValidator;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.utils.StringUtil;
+import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
@@ -178,8 +182,12 @@ public class SAMLEndpoint {
@GET
public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
@QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
+ @QueryParam(GeneralConstants.SAML_ARTIFACT_KEY) String samlArt,
@QueryParam(GeneralConstants.RELAY_STATE) String relayState) {
- return new RedirectBinding().execute(samlRequest, samlResponse, relayState, null);
+ if (Objects.isNull(samlArt)) {
+ return new RedirectBinding().execute(samlRequest, samlResponse, null, relayState, null);
+ }
+ return new ArtifactBinding().execute(samlRequest, samlResponse, samlArt, relayState, null);
}
@@ -189,17 +197,21 @@ public class SAMLEndpoint {
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
@FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
+ @FormParam(GeneralConstants.SAML_ARTIFACT_KEY) String samlArt,
@FormParam(GeneralConstants.RELAY_STATE) String relayState) {
- return new PostBinding().execute(samlRequest, samlResponse, relayState, null);
+ if (Objects.isNull(samlArt)) {
+ return new PostBinding().execute(samlRequest, samlResponse, null, relayState, null);
+ }
+ return new ArtifactBinding().execute(samlRequest, samlResponse, samlArt, relayState, null);
}
@Path("clients/{client_id}")
@GET
- public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
- @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
- @QueryParam(GeneralConstants.RELAY_STATE) String relayState,
- @PathParam("client_id") String clientId) {
- return new RedirectBinding().execute(samlRequest, samlResponse, relayState, clientId);
+ public Response redirectBindingIdpInitiated(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
+ @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
+ @QueryParam(GeneralConstants.RELAY_STATE) String relayState,
+ @PathParam("client_id") String clientId) {
+ return new RedirectBinding().execute(samlRequest, samlResponse, null, relayState, clientId);
}
@@ -208,11 +220,11 @@ public class SAMLEndpoint {
@Path("clients/{client_id}")
@POST
@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,
- @PathParam("client_id") String clientId) {
- return new PostBinding().execute(samlRequest, samlResponse, relayState, clientId);
+ public Response postBindingIdpInitiated(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
+ @FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
+ @FormParam(GeneralConstants.RELAY_STATE) String relayState,
+ @PathParam("client_id") String clientId) {
+ return new PostBinding().execute(samlRequest, samlResponse, null, relayState, clientId);
}
protected abstract class Binding {
@@ -224,7 +236,7 @@ public class SAMLEndpoint {
}
}
- protected Response basicChecks(String samlRequest, String samlResponse) {
+ protected Response basicChecks(String samlRequest, String samlResponse, String samlArt) {
if (!checkSsl()) {
event.event(EventType.LOGIN);
event.error(Errors.SSL_REQUIRED);
@@ -236,7 +248,7 @@ public class SAMLEndpoint {
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.REALM_NOT_ENABLED);
}
- if (samlRequest == null && samlResponse == null) {
+ if (samlRequest == null && samlResponse == null&& samlArt == null) {
event.event(EventType.LOGIN);
event.error(Errors.INVALID_REQUEST);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
@@ -280,11 +292,12 @@ public class SAMLEndpoint {
return new HardcodedKeyLocator(keys);
}
- public Response execute(String samlRequest, String samlResponse, String relayState, String clientId) {
+ public Response execute(String samlRequest, String samlResponse, String samlArt, String relayState, String clientId) {
event = new EventBuilder(realm, session, clientConnection);
- Response response = basicChecks(samlRequest, samlResponse);
+ Response response = basicChecks(samlRequest, samlResponse, samlArt);
if (response != null) return response;
if (samlRequest != null) return handleSamlRequest(samlRequest, relayState);
+ if (samlArt != null) return handleSamlArt(samlArt, relayState, clientId);
else return handleSamlResponse(samlResponse, relayState, clientId);
}
@@ -408,6 +421,73 @@ public class SAMLEndpoint {
}
+ protected Response handleSamlArt(String samlArt, String relayState, String clientId) {
+ try {
+ // execute the Resolve Artifact request
+ SAMLDocumentHolder samlDocumentHolder = provider.resolveArtifact(session, session.getContext().getUri(), realm, relayState, samlArt);
+
+ // validate the type of the SAML object
+ if (!(samlDocumentHolder.getSamlObject() instanceof ArtifactResponseType artifactResponse)) {
+ logger.error("artifact binding failed: the SAML object is not an ArtifactResponse");
+ event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
+ event.detail(Details.REASON, Errors.INVALID_SAML_ARTIFACT_RESPONSE);
+ event.error(Errors.INVALID_REQUEST);
+ return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
+ }
+
+ // validate the signature of the ArtifactResponse
+ if (config.isValidateSignature()) {
+ try {
+ verifySignature(GeneralConstants.SAML_RESPONSE_KEY, samlDocumentHolder);
+ } catch (VerificationException e) {
+ logger.error("artifact binding failed: the ArtifactResponse signature is invalid", e);
+ event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
+ event.error(Errors.INVALID_SIGNATURE);
+ return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.IDENTITY_PROVIDER_INVALID_SIGNATURE);
+ }
+ }
+
+ if (!(artifactResponse.getAny() instanceof ResponseType embeddedResponse)) {
+ logger.error("artifact binding failed: the embedded SAML object is not a Response");
+ event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
+ event.detail(Details.REASON, Errors.INVALID_SAML_ARTIFACT_RESPONSE);
+ event.error(Errors.INVALID_REQUEST);
+ return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
+ }
+
+ // validate the destination of the embedded Response
+ if (isDestinationRequired() && embeddedResponse.getDestination() == null && containsUnencryptedSignature(samlDocumentHolder)) {
+ logger.error("artifact binding failed: the embedded Response does not contain a destination");
+ event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
+ event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
+ event.error(Errors.INVALID_SAML_RESPONSE);
+ return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
+ }
+ if (!destinationValidator.validate(getExpectedDestination(config.getAlias(), clientId), embeddedResponse.getDestination())) {
+ logger.error("artifact binding failed: the embedded Response has an invalid destination");
+ event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
+ event.detail(Details.REASON, Errors.INVALID_DESTINATION);
+ event.error(Errors.INVALID_SAML_RESPONSE);
+ return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
+ }
+
+ // convert the embedded SAML response to a base64 serialized string
+ Document embeddedResponseAsDoc = SAML2Request.convert(embeddedResponse);
+ String embeddedResponseAsString = DocumentUtil.getDocumentAsString(embeddedResponseAsDoc);
+ logger.debugf("embeddedResponseAsString %s", embeddedResponseAsString);
+ String embeddedResponseAsBase64 = PostBindingUtil.base64Encode(embeddedResponseAsString);
+
+ // continue the flow with POST binding
+ return execute(null, embeddedResponseAsBase64, null, relayState, clientId);
+ } catch (IOException | ConfigurationException | ProcessingException | ParsingException e) {
+ logger.error("artifact binding failed", e);
+ event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
+ event.detail(Details.REASON, Errors.INVALID_SAML_ARTIFACT_RESPONSE);
+ event.error(Errors.INVALID_REQUEST);
+ return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
+ }
+ }
+
private Consumer processLogout(AtomicReference ref) {
return userSession -> {
for(Iterator it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext();) {
@@ -494,10 +574,15 @@ public class SAMLEndpoint {
return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.INVALID_REQUESTER);
}
+ // When artifact binding is used, the LoginResponse is embedded in the ArtifactResponse
+ // Therefore, the InResponseTo attribute of the LoginResponse cannot be validated
+ // Moreover, the LoginResponse is not signed
+ boolean isArtifactBinding = SamlProtocol.SAML_ARTIFACT_BINDING.equals(getBindingType());
+
// Validate InResponseTo attribute: must match the generated request ID
String expectedRequestId = authSession.getClientNote(SamlProtocol.SAML_REQUEST_ID_BROKER);
final boolean inResponseToValidationSuccess = validateInResponseToAttribute(responseType, expectedRequestId);
- if (!inResponseToValidationSuccess)
+ if (!isArtifactBinding && !inResponseToValidationSuccess)
{
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
event.error(Errors.INVALID_SAML_RESPONSE);
@@ -509,7 +594,7 @@ public class SAMLEndpoint {
final boolean signatureNotValid = signed && config.isValidateSignature() && !AssertionUtil.isSignatureValid(assertionElement, getIDPKeyLocator());
final boolean hasNoSignatureWhenRequired = ! signed && config.isValidateSignature() && ! containsUnencryptedSignature(holder);
- if (assertionSignatureNotExistsWhenRequired || signatureNotValid || hasNoSignatureWhenRequired) {
+ if (!isArtifactBinding && (assertionSignatureNotExistsWhenRequired || signatureNotValid || hasNoSignatureWhenRequired)) {
logger.error("validation failed");
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
event.error(Errors.INVALID_SIGNATURE);
@@ -806,6 +891,43 @@ public class SAMLEndpoint {
}
+ protected class ArtifactBinding extends Binding {
+ @Override
+ protected boolean containsUnencryptedSignature(SAMLDocumentHolder documentHolder) {
+ NodeList nl = documentHolder.getSamlDocument().getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
+ return (nl != null && nl.getLength() > 0);
+ }
+
+ @Override
+ protected void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException {
+ if ((! containsUnencryptedSignature(documentHolder)) && (documentHolder.getSamlObject() instanceof ResponseType)) {
+ ResponseType responseType = (ResponseType) documentHolder.getSamlObject();
+ List assertions = responseType.getAssertions();
+ if (! assertions.isEmpty() ) {
+ // Only relax verification if the response is an authnresponse and contains (encrypted/plaintext) assertion.
+ // In that case, signature is validated on assertion element
+ return;
+ }
+ }
+ SamlProtocolUtils.verifyDocumentSignature(documentHolder.getSamlDocument(), getIDPKeyLocator());
+ }
+
+ @Override
+ protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
+ throw new UnsupportedOperationException("SAML request is not compliant with Artifact binding");
+ }
+ @Override
+ protected SAMLDocumentHolder extractResponseDocument(String response) {
+ byte[] samlBytes = PostBindingUtil.base64Decode(response);
+ return SAMLRequestParser.parseResponseDocument(samlBytes);
+ }
+
+ @Override
+ protected String getBindingType() {
+ return SamlProtocol.SAML_ARTIFACT_BINDING;
+ }
+ }
+
private String getX500Attribute(AssertionType assertion, X500SAMLProfileConstants attribute) {
return getFirstMatchingAttribute(assertion, attribute::correspondsTo);
}
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
index f0705e85c19..d462d48cb86 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
@@ -16,6 +16,8 @@
*/
package org.keycloak.broker.saml;
+import jakarta.xml.soap.SOAPException;
+import jakarta.xml.soap.SOAPMessage;
import org.jboss.logging.Logger;
import org.keycloak.broker.provider.AbstractIdentityProvider;
import org.keycloak.broker.provider.AuthenticationRequest;
@@ -36,6 +38,7 @@ import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType;
import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType;
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
import org.keycloak.dom.saml.v2.metadata.LocalizedNameType;
+import org.keycloak.dom.saml.v2.protocol.ArtifactResolveType;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
@@ -46,7 +49,6 @@ import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.LoginProtocol;
@@ -59,6 +61,8 @@ import org.keycloak.protocol.saml.SamlSessionUtils;
import org.keycloak.protocol.saml.mappers.SamlMetadataDescriptorUpdater;
import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor;
import org.keycloak.protocol.saml.SAMLEncryptionAlgorithms;
+import org.keycloak.protocol.saml.profile.util.Soap;
+import org.keycloak.saml.SAML2ArtifactResolveRequestBuilder;
import org.keycloak.saml.SAML2AuthnRequestBuilder;
import org.keycloak.saml.SAML2LogoutRequestBuilder;
import org.keycloak.saml.SAML2NameIDPolicyBuilder;
@@ -69,10 +73,14 @@ 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.response.SAML2Response;
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
+import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLMetadataWriter;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.saml.validators.DestinationValidator;
@@ -138,7 +146,9 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider data = new HashMap<>();
data.put("providerId", "saml");
data.put("alias", "Alias With Space");
@@ -761,7 +761,7 @@ public class IdentityProviderTest extends AbstractAdminTest {
assertEqual(rep, providers.get(0));
}
-
+
@Test
public void testSamlImportAndExportDisabled() throws URISyntaxException, IOException, ParsingException {
@@ -784,7 +784,7 @@ public class IdentityProviderTest extends AbstractAdminTest {
IdentityProviderResource provider = realm.identityProviders().get("saml");
IdentityProviderRepresentation rep = provider.toRepresentation();
assertCreatedSamlIdp(rep, false);
-
+
}
@@ -1045,8 +1045,10 @@ public class IdentityProviderTest extends AbstractAdminTest {
"singleLogoutServiceUrl",
"postBindingLogout",
"postBindingResponse",
+ "artifactBindingResponse",
"postBindingAuthnRequest",
"singleSignOnServiceUrl",
+ "artifactResolutionServiceUrl",
"wantAuthnRequestsSigned",
"nameIDPolicyFormat",
"signingCertificate",
@@ -1057,7 +1059,9 @@ public class IdentityProviderTest extends AbstractAdminTest {
));
assertThat(config, hasEntry("validateSignature", "true"));
assertThat(config, hasEntry("singleLogoutServiceUrl", "http://localhost:8080/auth/realms/master/protocol/saml"));
+ assertThat(config, hasEntry("artifactResolutionServiceUrl", "http://localhost:8080/auth/realms/master/protocol/saml/resolve"));
assertThat(config, hasEntry("postBindingResponse", "true"));
+ assertThat(config, hasEntry("artifactBindingResponse", "false"));
assertThat(config, hasEntry("postBindingAuthnRequest", "true"));
assertThat(config, hasEntry("singleSignOnServiceUrl", "http://localhost:8080/auth/realms/master/protocol/saml"));
assertThat(config, hasEntry("wantAuthnRequestsSigned", "true"));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerArtifactBindingTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerArtifactBindingTest.java
new file mode 100644
index 00000000000..6ea838fed8c
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerArtifactBindingTest.java
@@ -0,0 +1,41 @@
+package org.keycloak.testsuite.broker;
+
+import org.junit.Test;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.broker.saml.SAMLIdentityProviderConfig;
+import org.keycloak.protocol.saml.SamlConfigAttributes;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.IdentityProviderRepresentation;
+
+public final class KcSamlBrokerArtifactBindingTest extends AbstractInitializedBaseBrokerTest {
+
+ @Override
+ protected BrokerConfiguration getBrokerConfiguration() {
+ return KcSamlBrokerConfiguration.INSTANCE;
+ }
+
+
+ @Test
+ public void testLogin() {
+ // configure artifact binding to the broker
+ IdentityProviderRepresentation idpRep = identityProviderResource.toRepresentation();
+ String baseSamlUrl = idpRep.getConfig().get(SAMLIdentityProviderConfig.ARTIFACT_RESOLUTION_SERVICE_URL);
+ idpRep.getConfig().put(SAMLIdentityProviderConfig.ARTIFACT_RESOLUTION_SERVICE_URL, baseSamlUrl + "/resolve");
+ idpRep.getConfig().put(SAMLIdentityProviderConfig.ARTIFACT_BINDING_RESPONSE, Boolean.TRUE.toString());
+ identityProviderResource.update(idpRep);
+
+ // configure artifact binding to the broker client
+ RealmResource providerRealm = realmsResouce().realm(bc.providerRealmName());
+ ClientRepresentation brokerClient = providerRealm.clients().findByClientId(bc.getIDPClientIdInProviderRealm()).get(0);
+ brokerClient.getAttributes().put(SamlConfigAttributes.SAML_ARTIFACT_BINDING, Boolean.TRUE.toString());
+ providerRealm.clients().get(brokerClient.getId()).update(brokerClient);
+
+ // login using artifact binding
+ oauth.clientId("broker-app");
+ loginPage.open(bc.consumerRealmName());
+ logInWithBroker(bc);
+ updateAccountInformationPage.assertCurrent();
+ updateAccountInformationPage.updateAccountInformation("f", "l");
+ appPage.assertCurrent();
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java
index ebaad04d597..08acffee0d7 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java
@@ -222,6 +222,7 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration {
config.put(IdentityProviderModel.SYNC_MODE, syncMode.toString());
config.put(SINGLE_SIGN_ON_SERVICE_URL, getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml");
+ config.put(ARTIFACT_RESOLUTION_SERVICE_URL, getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml");
config.put(SINGLE_LOGOUT_SERVICE_URL, getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml");
config.put(NAME_ID_POLICY_FORMAT, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress");
config.put(FORCE_AUTHN, "false");
@@ -231,6 +232,7 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration {
config.put(VALIDATE_SIGNATURE, "false");
config.put(WANT_AUTHN_REQUESTS_SIGNED, "false");
config.put(BACKCHANNEL_SUPPORTED, "false");
+ config.put(ARTIFACT_BINDING_RESPONSE, "false");
return idp;
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmUpdateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmUpdateTest.java
index 002e3ec7e43..f6fd3dd0796 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmUpdateTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmUpdateTest.java
@@ -42,11 +42,13 @@ public class KcAdmUpdateTest extends AbstractAdmCliTest {
.alias("idpAlias")
.displayName("SAML")
.setAttribute(SAMLIdentityProviderConfig.SINGLE_SIGN_ON_SERVICE_URL, "https://saml.idp/saml")
+ .setAttribute(SAMLIdentityProviderConfig.ARTIFACT_RESOLUTION_SERVICE_URL, "https://saml.idp/saml")
.setAttribute(SAMLIdentityProviderConfig.SINGLE_LOGOUT_SERVICE_URL, "https://saml.idp/saml")
.setAttribute(SAMLIdentityProviderConfig.NAME_ID_POLICY_FORMAT, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress")
.setAttribute(SAMLIdentityProviderConfig.POST_BINDING_RESPONSE, "false")
.setAttribute(SAMLIdentityProviderConfig.POST_BINDING_AUTHN_REQUEST, "false")
.setAttribute(SAMLIdentityProviderConfig.BACKCHANNEL_SUPPORTED, "false")
+ .setAttribute(SAMLIdentityProviderConfig.ARTIFACT_BINDING_RESPONSE, "false")
.build();
try (Closeable ipc = new IdentityProviderCreator(realmResource, identityProvider)) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BrokerTest.java
index ffca2c78871..df81555de43 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BrokerTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BrokerTest.java
@@ -52,6 +52,7 @@ import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
import org.keycloak.testsuite.updaters.IdentityProviderCreator;
import org.keycloak.testsuite.util.IdentityProviderBuilder;
import org.keycloak.testsuite.util.SamlClientBuilder;
+
import java.io.IOException;
import java.net.URI;
import java.security.KeyPair;
@@ -67,6 +68,7 @@ import org.apache.http.HttpHeaders;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Test;
+import org.keycloak.testsuite.util.saml.SamlBackchannelArtifactResolveReceiver;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
@@ -75,10 +77,8 @@ import org.w3c.dom.NodeList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.fail;
import static org.keycloak.saml.SignatureAlgorithm.RSA_SHA1;
-import static org.keycloak.testsuite.saml.AbstractSamlTest.REALM_NAME;
-import static org.keycloak.testsuite.saml.AbstractSamlTest.SAML_ASSERTION_CONSUMER_URL_SALES_POST;
-import static org.keycloak.testsuite.saml.AbstractSamlTest.SAML_CLIENT_ID_SALES_POST;
import static org.keycloak.testsuite.util.Matchers.isSamlStatusResponse;
import static org.keycloak.testsuite.util.SamlClient.Binding.POST;
import static org.keycloak.testsuite.util.SamlClient.Binding.REDIRECT;
@@ -95,11 +95,13 @@ public class BrokerTest extends AbstractSamlTest {
.alias(SAML_BROKER_ALIAS)
.displayName("SAML")
.setAttribute(SAMLIdentityProviderConfig.SINGLE_SIGN_ON_SERVICE_URL, samlEndpoint)
+ .setAttribute(SAMLIdentityProviderConfig.ARTIFACT_RESOLUTION_SERVICE_URL, samlEndpoint)
.setAttribute(SAMLIdentityProviderConfig.SINGLE_LOGOUT_SERVICE_URL, samlEndpoint)
.setAttribute(SAMLIdentityProviderConfig.NAME_ID_POLICY_FORMAT, JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())
.setAttribute(SAMLIdentityProviderConfig.POST_BINDING_RESPONSE, "false")
.setAttribute(SAMLIdentityProviderConfig.POST_BINDING_AUTHN_REQUEST, "false")
.setAttribute(SAMLIdentityProviderConfig.BACKCHANNEL_SUPPORTED, "false")
+ .setAttribute(SAMLIdentityProviderConfig.ARTIFACT_BINDING_RESPONSE, "false")
.build();
return identityProvider;
}
@@ -446,4 +448,48 @@ public class BrokerTest extends AbstractSamlTest {
.execute();
}
}
+
+ @Test
+ public void testResolveArtifactBindingAsSp() {
+ RealmResource realm = adminClient.realm(REALM_NAME);
+
+ try (SamlBackchannelArtifactResolveReceiver samlBackchannelArtifactResolveReceiver = new SamlBackchannelArtifactResolveReceiver(
+ 8082,
+ realm.clients().findByClientId(SAML_CLIENT_ID_SALES_POST).get(0)
+ )) {
+
+ IdentityProviderRepresentation rep = addIdentityProvider("https://saml.idp/saml");
+ rep.getConfig().put(SAMLIdentityProviderConfig.ARTIFACT_RESOLUTION_SERVICE_URL, samlBackchannelArtifactResolveReceiver.getUrl());
+ rep.getConfig().put(SAMLIdentityProviderConfig.ARTIFACT_BINDING_RESPONSE, "true");
+
+ try (IdentityProviderCreator idp = new IdentityProviderCreator(realm, rep)) {
+ SamlClientBuilder samlClientBuilder = new SamlClientBuilder();
+
+ // trigger authentication
+ 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();
+
+ // simulate login page interaction
+ samlClientBuilder.login().idp(SAML_BROKER_ALIAS).build();
+
+ // simulate IdP response (artifact as query param)
+ samlClientBuilder.processSamlResponse(REDIRECT)
+ .targetAttributeSamlArtifact()
+ .targetUri(getSamlBrokerUrl(REALM_NAME))
+ .build();
+
+ // assert the authentication succeeded
+ samlClientBuilder.assertResponse(org.keycloak.testsuite.util.Matchers.statusCodeIsHC(Status.OK));
+
+ samlClientBuilder.execute();
+ }
+ } catch (Exception ex) {
+ fail("unexpected error");
+ }
+ }
+
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java
index b1719f0c2cd..d1cb51e180d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java
@@ -86,6 +86,7 @@ public class LogoutTest extends AbstractSamlTest {
private static final String NAME_QUALIFIER = "nameQualifier";
private static final String BROKER_SIGN_ON_SERVICE_URL = "https://saml.idp/saml";
+ private static final String BROKER_SIGN_ON_ARTIFACT_SERVICE_URL = "https://saml.idp/saml";
private static final String BROKER_LOGOUT_SERVICE_URL = "https://saml.idp/SLO/saml";
private static final String BROKER_SERVICE_ID = "https://saml.idp/saml";
@@ -508,11 +509,13 @@ public class LogoutTest extends AbstractSamlTest {
.alias(SAML_BROKER_ALIAS)
.displayName("SAML")
.setAttribute(SAMLIdentityProviderConfig.SINGLE_SIGN_ON_SERVICE_URL, BROKER_SIGN_ON_SERVICE_URL)
+ .setAttribute(SAMLIdentityProviderConfig.ARTIFACT_RESOLUTION_SERVICE_URL, BROKER_SIGN_ON_ARTIFACT_SERVICE_URL)
.setAttribute(SAMLIdentityProviderConfig.SINGLE_LOGOUT_SERVICE_URL, BROKER_LOGOUT_SERVICE_URL)
.setAttribute(SAMLIdentityProviderConfig.NAME_ID_POLICY_FORMAT, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress")
.setAttribute(SAMLIdentityProviderConfig.POST_BINDING_RESPONSE, "false")
.setAttribute(SAMLIdentityProviderConfig.POST_BINDING_AUTHN_REQUEST, "false")
.setAttribute(SAMLIdentityProviderConfig.BACKCHANNEL_SUPPORTED, "false")
+ .setAttribute(SAMLIdentityProviderConfig.ARTIFACT_BINDING_RESPONSE, "false")
.build();
return identityProvider;
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata-disabled.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata-disabled.xml
index 963107282d6..bd7f4c08894 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata-disabled.xml
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata-disabled.xml
@@ -34,5 +34,8 @@
+
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata-encryption-methods.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata-encryption-methods.xml
index d06d37efb92..464bdb4c4bb 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata-encryption-methods.xml
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata-encryption-methods.xml
@@ -36,5 +36,8 @@
urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
+
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata-two-signing-certs.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata-two-signing-certs.xml
index 74618cf035c..c703144df83 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata-two-signing-certs.xml
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata-two-signing-certs.xml
@@ -44,5 +44,8 @@
urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
+
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata.xml
index a44e816a37a..ec29ef2c42c 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata.xml
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata.xml
@@ -32,5 +32,8 @@
urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
+