feat(SAML): add Artifact Binding on brokering scenarios when Keycloak is SP (#29619)

* feat: add Artifact Binding on brokering scenarios when Keycloak is SP

Signed-off-by: tmorin <git@morin.io>

* Adding broker test and minor improvements

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>

* Fixing IdentityProviderTest

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>

* Renaming methods related to idp initiated flows

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>

* Fixing partial_import_test.spec.ts

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>

---------

Signed-off-by: tmorin <git@morin.io>
Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
Co-authored-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Thibault Morin 2024-06-14 13:54:49 +02:00 committed by GitHub
parent 17cd18a6da
commit f6fa869b12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 655 additions and 32 deletions

View file

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

View file

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

View file

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

View file

@ -70,6 +70,13 @@ const Fields = ({ readOnly }: DescriptorSettingsProps) => {
readOnly={readOnly}
rules={{ required: t("required") }}
/>
<TextControl
name="config.artifactResolutionServiceUrl"
label={t("artifactResolutionServiceUrl")}
labelIcon={t("artifactResolutionServiceUrlHelp")}
type="url"
isDisabled={readOnly}
/>
<TextControl
name="config.singleLogoutServiceUrl"
label={t("singleLogoutServiceUrl")}
@ -174,6 +181,13 @@ const Fields = ({ readOnly }: DescriptorSettingsProps) => {
stringify
/>
<DefaultSwitchControl
name="config.artifactBindingResponse"
label={t("artifactBindingResponse")}
isDisabled={readOnly}
stringify
/>
<DefaultSwitchControl
name="config.postBindingAuthnRequest"
label={t("httpPostBindingAuthnRequest")}

View file

@ -0,0 +1,92 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.saml;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.protocol.ArtifactResolveType;
import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
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.w3c.dom.Document;
import java.net.URI;
import java.util.LinkedList;
import java.util.List;
public class SAML2ArtifactResolveRequestBuilder implements SamlProtocolExtensionsAwareBuilder<SAML2ArtifactResolveRequestBuilder> {
protected String artifact;
protected String destination;
protected NameIDType issuer;
protected final List<NodeGenerator> 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;
}
}

View file

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

View file

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

View file

@ -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,
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, relayState, 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,
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, relayState, 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<UserSessionModel> processLogout(AtomicReference<LogoutRequestType> ref) {
return userSession -> {
for(Iterator<SamlAuthenticationPreprocessor> 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<ResponseType.RTChoiceType> 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);
}

View file

@ -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<SAMLIdentityP
String assertionConsumerServiceUrl = request.getRedirectUri();
if (getConfig().isPostBindingResponse()) {
if (getConfig().isArtifactBindingResponse()) {
protocolBinding = JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get();
} else if (getConfig().isPostBindingResponse()) {
protocolBinding = JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get();
}
@ -525,4 +535,65 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
// SAML RelayState parameter has limits of 80 bytes per SAML specification
return false;
}
public SAMLDocumentHolder resolveArtifact(KeycloakSession session, UriInfo uriInfo, RealmModel realm, String relayState, String samlArt) {
//get the URL of the artifact resolution service provided by the Identity Provider
String artifactResolutionServiceUrl = getConfig().getArtifactResolutionServiceUrl();
if (artifactResolutionServiceUrl == null || artifactResolutionServiceUrl.trim().isEmpty()) {
throw new RuntimeException("Artifact Resolution Service URL is not configured for the Identity Provider.");
}
try {
// create the SAML Request object to resolve an artifact
ArtifactResolveType artifactResolveRequest = buildArtifactResolveRequest(uriInfo, realm, artifactResolutionServiceUrl, samlArt);
if (artifactResolveRequest.getDestination() != null) {
artifactResolutionServiceUrl = artifactResolveRequest.getDestination().toString();
}
// convert the SAML Request object to a SAML Document (DOM)
Document artifactResolveRequestAsDoc = SAML2Request.convert(artifactResolveRequest);
// convert the SAML Document (DOM) to a SOAP Document (DOM)
Document soapRequestAsDoc = buildArtifactResolveBinding(session, relayState, realm)
.soapBinding(artifactResolveRequestAsDoc).getDocument();
// execute the SOAP request
SOAPMessage soapResponse = Soap.createMessage()
.addMimeHeader("SOAPAction", "http://www.oasis-open.org/committees/security") // MAY in SOAP binding spec
.addToBody(soapRequestAsDoc)
.call(artifactResolutionServiceUrl, session);
// extract the SAML Response (DOM) from the SOAP response
Document artifactResolveResponseAsDoc = Soap.extractSoapMessage(soapResponse);
// convert the SAML Response (DOM) to a SAML Response object and return it
return SAML2Response.getSAML2ObjectFromDocument(artifactResolveResponseAsDoc);
} catch (SOAPException | ConfigurationException | ProcessingException | ParsingException e) {
logger.warn("Unable to resolve a SAML artifact to: " + artifactResolutionServiceUrl, e);
throw new RuntimeException("Unable to resolve a SAML artifact to: " + artifactResolutionServiceUrl, e);
}
}
protected ArtifactResolveType buildArtifactResolveRequest(UriInfo uriInfo, RealmModel realm, String artifactServiceUrl, String artifact, NodeGenerator... extensions) throws ConfigurationException {
SAML2ArtifactResolveRequestBuilder artifactResolveRequestBuilder = new SAML2ArtifactResolveRequestBuilder()
.issuer(getEntityId(uriInfo, realm))
.destination(artifactServiceUrl)
.artifact(artifact);
ArtifactResolveType artifactResolveRequest = artifactResolveRequestBuilder.createArtifactResolveRequest();
for (NodeGenerator extension : extensions) {
artifactResolveRequestBuilder.addExtension(extension);
}
return artifactResolveRequest;
}
private JaxrsSAML2BindingBuilder buildArtifactResolveBinding(KeycloakSession session, String relayState, RealmModel realm) {
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(session).relayState(relayState);
if (getConfig().isWantAuthnRequestsSigned()) {
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
String keyName = getConfig().getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
binding.signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate())
.signatureAlgorithm(getSignatureAlgorithm())
.signDocument();
}
return binding;
}
}

View file

@ -44,11 +44,13 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
public static final String POST_BINDING_AUTHN_REQUEST = "postBindingAuthnRequest";
public static final String POST_BINDING_LOGOUT = "postBindingLogout";
public static final String POST_BINDING_RESPONSE = "postBindingResponse";
public static final String ARTIFACT_BINDING_RESPONSE = "artifactBindingResponse";
public static final String SIGNATURE_ALGORITHM = "signatureAlgorithm";
public static final String ENCRYPTION_ALGORITHM = "encryptionAlgorithm";
public static final String SIGNING_CERTIFICATE_KEY = "signingCertificate";
public static final String SINGLE_LOGOUT_SERVICE_URL = "singleLogoutServiceUrl";
public static final String SINGLE_SIGN_ON_SERVICE_URL = "singleSignOnServiceUrl";
public static final String ARTIFACT_RESOLUTION_SERVICE_URL = "artifactResolutionServiceUrl";
public static final String VALIDATE_SIGNATURE = "validateSignature";
public static final String PRINCIPAL_TYPE = "principalType";
public static final String PRINCIPAL_ATTRIBUTE = "principalAttribute";
@ -97,6 +99,14 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
getConfig().put(SINGLE_SIGN_ON_SERVICE_URL, singleSignOnServiceUrl);
}
public String getArtifactResolutionServiceUrl() {
return getConfig().get(ARTIFACT_RESOLUTION_SERVICE_URL);
}
public void setArtifactResolutionServiceUrl(String artifactResolutionServiceUrl) {
getConfig().put(ARTIFACT_RESOLUTION_SERVICE_URL, artifactResolutionServiceUrl);
}
public String getSingleLogoutServiceUrl() {
return getConfig().get(SINGLE_LOGOUT_SERVICE_URL);
}
@ -260,6 +270,14 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
getConfig().put(BACKCHANNEL_SUPPORTED, String.valueOf(backchannel));
}
public boolean isArtifactBindingResponse() {
return Boolean.valueOf(getConfig().get(ARTIFACT_BINDING_RESPONSE));
}
public void setArtifactBindingResponse(boolean backchannel) {
getConfig().put(ARTIFACT_BINDING_RESPONSE, String.valueOf(backchannel));
}
/**
* Always returns non-{@code null} result.
* @return Configured ransformer of {@link #DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER} if not set.
@ -424,6 +442,9 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
throw new IllegalArgumentException(USE_METADATA_DESCRIPTOR_URL + " needs a non-empty URL for " + METADATA_DESCRIPTOR_URL);
}
}
if (StringUtil.isNotBlank(getArtifactResolutionServiceUrl())) {
checkUrl(sslRequired, getArtifactResolutionServiceUrl(), ARTIFACT_RESOLUTION_SERVICE_URL);
}
//transient name id format is not accepted together with principaltype SubjectnameId
if (JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get().equals(getNameIDPolicyFormat()) && SamlPrincipalType.SUBJECT == getPrincipalType())
throw new IllegalArgumentException("Can not have Transient NameID Policy Format together with SUBJECT Principal Type");

View file

@ -99,13 +99,23 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
}
}
String artifactResolutionServiceUrl = null;
boolean artifactBindingResponse = false;
for (EndpointType endpoint : idpDescriptor.getArtifactResolutionService()) {
if (endpoint.getBinding().toString().equals(JBossSAMLURIConstants.SAML_SOAP_BINDING.get())) {
artifactResolutionServiceUrl = endpoint.getLocation().toString();
break;
}
}
samlIdentityProviderConfig.setIdpEntityId(entityType.getEntityID());
samlIdentityProviderConfig.setSingleLogoutServiceUrl(singleLogoutServiceUrl);
samlIdentityProviderConfig.setArtifactResolutionServiceUrl(artifactResolutionServiceUrl);
samlIdentityProviderConfig.setSingleSignOnServiceUrl(singleSignOnServiceUrl);
samlIdentityProviderConfig.setWantAuthnRequestsSigned(idpDescriptor.isWantAuthnRequestsSigned());
samlIdentityProviderConfig.setAddExtensionsElementWithKeyInfo(false);
samlIdentityProviderConfig.setValidateSignature(idpDescriptor.isWantAuthnRequestsSigned());
samlIdentityProviderConfig.setPostBindingResponse(postBindingResponse);
samlIdentityProviderConfig.setArtifactBindingResponse(artifactBindingResponse);
samlIdentityProviderConfig.setPostBindingAuthnRequest(postBindingResponse);
samlIdentityProviderConfig.setPostBindingLogout(postBindingLogout);
samlIdentityProviderConfig.setLoginHint(false);

View file

@ -133,6 +133,7 @@ public class SamlProtocol implements LoginProtocol {
public static final String SAML_BINDING = "saml_binding";
public static final String SAML_IDP_INITIATED_LOGIN = "saml_idp_initiated_login";
public static final String SAML_POST_BINDING = "post";
public static final String SAML_ARTIFACT_BINDING = "artifact";
public static final String SAML_SOAP_BINDING = "soap";
public static final String SAML_REDIRECT_BINDING = "get";
public static final String SAML_REQUEST_ID = "SAML_REQUEST_ID";

View file

@ -127,6 +127,10 @@ public class ModifySamlResponseStepBuilder extends SamlDocumentStepBuilder<SAML2
return targetAttribute(GeneralConstants.SAML_RESPONSE_KEY);
}
public ModifySamlResponseStepBuilder targetAttributeSamlArtifact() {
return targetAttribute(GeneralConstants.SAML_ARTIFACT_KEY);
}
public URI targetUri() {
return targetUri;
}

View file

@ -0,0 +1,145 @@
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 jakarta.ws.rs.core.HttpHeaders;
import org.jboss.logging.Logger;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.dom.saml.v2.protocol.ArtifactResolveType;
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder;
import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.protocol.saml.SamlProtocolUtils;
import org.keycloak.protocol.saml.profile.util.Soap;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.saml.SAML2LoginResponseBuilder;
import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.w3c.dom.Document;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.security.PrivateKey;
import java.security.PublicKey;
public class SamlBackchannelArtifactResolveReceiver implements AutoCloseable {
private static final Logger LOG = Logger.getLogger(SamlBackchannelArtifactResolveReceiver.class);
private final HttpServer server;
private ArtifactResolveType artifactResolve;
private final String url;
private final ClientRepresentation samlClient;
private final PublicKey publicKey;
private final PrivateKey privateKey;
public SamlBackchannelArtifactResolveReceiver(int port, ClientRepresentation samlClient, String publicKeyStr, String privateKeyStr) {
this.samlClient = samlClient;
publicKey = publicKeyStr == null ? null : org.keycloak.testsuite.util.KeyUtils.publicKeyFromString(publicKeyStr);
privateKey = privateKeyStr == null ? null : org.keycloak.testsuite.util.KeyUtils.privateKeyFromString(privateKeyStr);
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 SamlBackchannelArtifactResolveHandler());
server.setExecutor(null);
server.start();
}
public SamlBackchannelArtifactResolveReceiver(int port, ClientRepresentation samlClient) {
this(port, samlClient, null, null);
}
public String getUrl() {
return url;
}
public boolean isArtifactResolveReceived() {
return artifactResolve != null;
}
public ArtifactResolveType getArtifactResolve() {
return artifactResolve;
}
@Override
public void close() throws Exception {
server.stop(0);
}
private class SamlBackchannelArtifactResolveHandler implements HttpHandler {
public void handle(HttpExchange t) throws IOException {
try {
t.getResponseHeaders().add(HttpHeaders.CONTENT_TYPE, "text/xml");
t.sendResponseHeaders(200, 0);
Document request = Soap.extractSoapMessage(t.getRequestBody());
LOG.infof("Received ArtifactResolve: %s", DocumentUtil.asString(request));
SAMLDocumentHolder samlDoc = SAML2Response.getSAML2ObjectFromDocument(request);
if (!(samlDoc.getSamlObject() instanceof ArtifactResolveType)) {
throw new RuntimeException("SamlBackchannelArtifactResolveReceiver received a message that was not ArtifactResolveType");
}
artifactResolve = (ArtifactResolveType) samlDoc.getSamlObject();
// create the login response
SAML2LoginResponseBuilder loginResponseBuilder = new SAML2LoginResponseBuilder();
ResponseType loginResponse = loginResponseBuilder
.issuer(samlClient.getClientId())
.requestIssuer(artifactResolve.getIssuer().getValue())
.requestID(artifactResolve.getID())
.buildModel();
Document loginResponseBuilderAsDoc = loginResponseBuilder.buildDocument(loginResponse);
// bundle the login response in the Artifact Response
ArtifactResponseType artifactResponse = SamlProtocolUtils.buildArtifactResponse(loginResponseBuilderAsDoc);
artifactResponse.setInResponseTo(artifactResolve.getID());
JaxrsSAML2BindingBuilder soapBinding = new JaxrsSAML2BindingBuilder(null);
if (requiresClientSignature(samlClient)) {
soapBinding.signatureAlgorithm(getSignatureAlgorithm(samlClient))
.signWith(KeyUtils.createKeyId(privateKey), privateKey, publicKey, null)
.signDocument();
}
Document artifactResponseAsDoc = SAML2Response.convert(artifactResponse);
Document soapDoc = soapBinding.soapBinding(artifactResponseAsDoc).getDocument();
LOG.infof("Sending ArtifactResponse: %s", DocumentUtil.asString(soapDoc));
// send login response
OutputStream os = t.getResponseBody();
os.write(Soap.createMessage().addToBody(soapDoc).getBytes());
os.close();
} catch (Exception ex) {
t.sendResponseHeaders(500, 0);
}
}
}
private SignatureAlgorithm getSignatureAlgorithm(ClientRepresentation client) {
String alg = client.getAttributes().get(SamlConfigAttributes.SAML_SIGNATURE_ALGORITHM);
if (alg != null) {
SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(alg);
if (algorithm != null)
return algorithm;
}
return SignatureAlgorithm.RSA_SHA256;
}
public boolean requiresClientSignature(ClientRepresentation client) {
return "true".equals(client.getAttributes().get(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE));
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -34,5 +34,8 @@
</dsig:X509Data>
</dsig:KeyInfo>
</KeyDescriptor>
<ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="http://localhost:8080/auth/realms/master/protocol/saml/resolve"
index="0"/>
</IDPSSODescriptor>
</EntityDescriptor>

View file

@ -36,5 +36,8 @@
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://localhost:8080/auth/realms/master/protocol/saml" />
<ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="http://localhost:8080/auth/realms/master/protocol/saml/resolve"
index="0"/>
</IDPSSODescriptor>
</EntityDescriptor>

View file

@ -44,5 +44,8 @@
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://localhost:8080/auth/realms/master/protocol/saml" />
<ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="http://localhost:8080/auth/realms/master/protocol/saml/resolve"
index="0"/>
</IDPSSODescriptor>
</EntityDescriptor>

View file

@ -32,5 +32,8 @@
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://localhost:8080/auth/realms/master/protocol/saml" />
<ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="http://localhost:8080/auth/realms/master/protocol/saml/resolve"
index="0"/>
</IDPSSODescriptor>
</EntityDescriptor>