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:
parent
17cd18a6da
commit
f6fa869b12
24 changed files with 655 additions and 32 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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")}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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"));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue