keycloak-memoizeit
Changes
adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java 5(+5 -0)
adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java 11(+10 -1)
saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/response/SAML2Response.java 82(+4 -78)
saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/factories/JBossSAMLAuthnResponseFactory.java 6(+2 -4)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlIdPInitiatedSsoTest.java 73(+57 -16)
Details
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java
index 2bae779..c5cdb2c 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java
@@ -68,6 +68,11 @@ public class DeploymentBuilder {
SP sp = adapter.getSps().get(0);
deployment.setConfigured(true);
deployment.setEntityID(sp.getEntityID());
+ try {
+ URI.create(sp.getEntityID());
+ } catch (IllegalArgumentException ex) {
+ log.warnf("Entity ID is not an URI, assertion that restricts audience will fail. Update Entity ID to be URI.", sp.getEntityID());
+ }
deployment.setForceAuthentication(sp.isForceAuthentication());
deployment.setIsPassive(sp.isIsPassive());
deployment.setNameIDPolicyFormat(sp.getNameIDPolicyFormat());
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java
index da55815..9a159df 100644
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java
@@ -84,6 +84,7 @@ import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
import org.keycloak.rotation.KeyLocator;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.saml.processing.core.util.XMLEncryptionUtil;
+import org.keycloak.saml.validators.ConditionsValidator;
import org.keycloak.saml.validators.DestinationValidator;
/**
@@ -342,7 +343,15 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
}
try {
assertion = AssertionUtil.getAssertion(responseHolder, responseType, deployment.getDecryptionKey());
- if (AssertionUtil.hasExpired(assertion)) {
+ ConditionsValidator.Builder cvb = new ConditionsValidator.Builder(assertion.getID(), assertion.getConditions(), destinationValidator);
+ try {
+ cvb.addAllowedAudience(URI.create(deployment.getEntityID()));
+ // getDestination has been validated to match request URL already so it matches SAML endpoint
+ cvb.addAllowedAudience(URI.create(responseType.getDestination()));
+ } catch (IllegalArgumentException ex) {
+ // warning has been already emitted in DeploymentBuilder
+ }
+ if (! cvb.build().isValid()) {
return initiateLogin();
}
} catch (Exception e) {
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/response/SAML2Response.java b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/response/SAML2Response.java
index ab78fff..4f356e9 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/response/SAML2Response.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/response/SAML2Response.java
@@ -151,71 +151,6 @@ public class SAML2Response {
}
/**
- * Construct a {@link ResponseType} without calling PicketLink STS for the assertion. The {@link AssertionType} is
- * generated
- * within this method
- *
- * @param ID id of the {@link ResponseType}
- * @param sp
- * @param idp
- * @param issuerInfo
- *
- * @return
- *
- * @throws org.keycloak.saml.common.exceptions.ConfigurationException
- * @throws org.keycloak.saml.common.exceptions.ProcessingException
- */
- public ResponseType createResponseType(String ID, SPInfoHolder sp, IDPInfoHolder idp, IssuerInfoHolder issuerInfo,
- AssertionType assertion) throws ConfigurationException, ProcessingException {
- String responseDestinationURI = sp.getResponseDestinationURI();
-
- XMLGregorianCalendar issueInstant = XMLTimeUtil.getIssueInstant();
-
- // Create assertion -> subject
- SubjectType subjectType = new SubjectType();
-
- // subject -> nameid
- NameIDType nameIDType = new NameIDType();
- nameIDType.setFormat(URI.create(idp.getNameIDFormat()));
- nameIDType.setValue(idp.getNameIDFormatValue());
-
- SubjectType.STSubType subType = new SubjectType.STSubType();
- subType.addBaseID(nameIDType);
- subjectType.setSubType(subType);
-
- SubjectConfirmationType subjectConfirmation = new SubjectConfirmationType();
- subjectConfirmation.setMethod(idp.getSubjectConfirmationMethod());
-
- SubjectConfirmationDataType subjectConfirmationData = new SubjectConfirmationDataType();
- subjectConfirmationData.setInResponseTo(sp.getRequestID());
- subjectConfirmationData.setRecipient(responseDestinationURI);
- //subjectConfirmationData.setNotBefore(issueInstant);
- subjectConfirmationData.setNotOnOrAfter(issueInstant);
-
- subjectConfirmation.setSubjectConfirmationData(subjectConfirmationData);
-
- subjectType.addConfirmation(subjectConfirmation);
-
- ConditionsType conditions = assertion.getConditions();
- // Update the subjectConfirmationData expiry based on the assertion
- if (conditions != null) {
- subjectConfirmationData.setNotOnOrAfter(conditions.getNotOnOrAfter());
- //Add conditions -> AudienceRestriction
- AudienceRestrictionType audience = new AudienceRestrictionType();
- audience.addAudience(URI.create(sp.getResponseDestinationURI()));
- conditions.addCondition(audience);
- }
-
- ResponseType responseType = createResponseType(ID, issuerInfo, assertion);
- // InResponseTo ID
- responseType.setInResponseTo(sp.getRequestID());
- // Destination
- responseType.setDestination(responseDestinationURI);
-
- return responseType;
- }
-
- /**
* Create a ResponseType
*
* <b>NOTE:</b>: The PicketLink STS is used to issue/update the assertion
@@ -234,7 +169,7 @@ public class SAML2Response {
* @throws ProcessingException
*/
public ResponseType createResponseType(String ID, SPInfoHolder sp, IDPInfoHolder idp, IssuerInfoHolder issuerInfo)
- throws ConfigurationException, ProcessingException {
+ throws ProcessingException {
String responseDestinationURI = sp.getResponseDestinationURI();
XMLGregorianCalendar issueInstant = XMLTimeUtil.getIssueInstant();
@@ -266,11 +201,7 @@ public class SAML2Response {
AssertionType assertionType;
NameIDType issuerID = issuerInfo.getIssuer();
- try {
- issueInstant = XMLTimeUtil.getIssueInstant();
- } catch (ConfigurationException e) {
- throw logger.processingError(e);
- }
+ issueInstant = XMLTimeUtil.getIssueInstant();
ConditionsType conditions = null;
List<StatementAbstractType> statements = new LinkedList<>();
@@ -303,11 +234,7 @@ public class SAML2Response {
* @return
*/
public ResponseType createResponseType(String ID) {
- try {
- return new ResponseType(ID, XMLTimeUtil.getIssueInstant());
- } catch (ConfigurationException e) {
- throw new RuntimeException(e);
- }
+ return new ResponseType(ID, XMLTimeUtil.getIssueInstant());
}
/**
@@ -321,8 +248,7 @@ public class SAML2Response {
*
* @throws ConfigurationException
*/
- public ResponseType createResponseType(String ID, IssuerInfoHolder issuerInfo, AssertionType assertion)
- throws ConfigurationException {
+ public ResponseType createResponseType(String ID, IssuerInfoHolder issuerInfo, AssertionType assertion){
return JBossSAMLAuthnResponseFactory.createResponseType(ID, issuerInfo, assertion);
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/factories/JBossSAMLAuthnResponseFactory.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/factories/JBossSAMLAuthnResponseFactory.java
index 1adc704..53853cb 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/factories/JBossSAMLAuthnResponseFactory.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/factories/JBossSAMLAuthnResponseFactory.java
@@ -164,8 +164,7 @@ public class JBossSAMLAuthnResponseFactory {
*
* @throws ConfigurationException
*/
- public static ResponseType createResponseType(String ID, IssuerInfoHolder issuerInfo, AssertionType assertionType)
- throws ConfigurationException {
+ public static ResponseType createResponseType(String ID, IssuerInfoHolder issuerInfo, AssertionType assertionType) {
XMLGregorianCalendar issueInstant = XMLTimeUtil.getIssueInstant();
ResponseType responseType = new ResponseType(ID, issueInstant);
@@ -195,8 +194,7 @@ public class JBossSAMLAuthnResponseFactory {
*
* @throws ConfigurationException
*/
- public static ResponseType createResponseType(String ID, IssuerInfoHolder issuerInfo, Element encryptedAssertion)
- throws ConfigurationException {
+ public static ResponseType createResponseType(String ID, IssuerInfoHolder issuerInfo, Element encryptedAssertion) {
ResponseType responseType = new ResponseType(ID, XMLTimeUtil.getIssueInstant());
// Issuer
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
index 887df88..640509f 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
@@ -43,7 +43,6 @@ import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.exceptions.fed.IssueInstantMissingException;
import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.common.util.StaxUtil;
-import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLAssertionWriter;
@@ -140,12 +139,7 @@ public class AssertionUtil {
* @return
*/
public static AssertionType createAssertion(String id, NameIDType issuer) {
- XMLGregorianCalendar issueInstant = null;
- try {
- issueInstant = XMLTimeUtil.getIssueInstant();
- } catch (ConfigurationException e) {
- throw new RuntimeException(e);
- }
+ XMLGregorianCalendar issueInstant = XMLTimeUtil.getIssueInstant();
AssertionType assertion = new AssertionType(id, issueInstant);
assertion.setIssuer(issuer);
return assertion;
@@ -320,7 +314,8 @@ public class AssertionUtil {
}
/**
- * Check whether the assertion has expired
+ * Check whether the assertion has expired.
+ * Processing rules defined in Section 2.5.1.2 of saml-core-2.0-os.pdf.
*
* @param assertion
*
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/XMLTimeUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/XMLTimeUtil.java
index 7b45cae..c194eeb 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/XMLTimeUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/XMLTimeUtil.java
@@ -46,17 +46,25 @@ public class XMLTimeUtil {
* Add additional time in miliseconds
*
* @param value calendar whose value needs to be updated
- * @param milis
+ * @param millis
*
* @return calendar value with the addition
*
* @throws org.keycloak.saml.common.exceptions.ConfigurationException
*/
- public static XMLGregorianCalendar add(XMLGregorianCalendar value, long milis) {
+ public static XMLGregorianCalendar add(XMLGregorianCalendar value, long millis) {
+ if (value == null) {
+ return null;
+ }
+
XMLGregorianCalendar newVal = (XMLGregorianCalendar) value.clone();
+ if (millis == 0) {
+ return newVal;
+ }
+
Duration duration;
- duration = DATATYPE_FACTORY.get().newDuration(milis);
+ duration = DATATYPE_FACTORY.get().newDuration(millis);
newVal.add(duration);
return newVal;
}
@@ -65,16 +73,14 @@ public class XMLTimeUtil {
* Subtract some miliseconds from the time value
*
* @param value
- * @param milis miliseconds entered in a positive value
+ * @param millis miliseconds entered in a positive value
*
* @return
*
* @throws ConfigurationException
*/
- public static XMLGregorianCalendar subtract(XMLGregorianCalendar value, long milis) {
- if (milis < 0)
- throw logger.invalidArgumentError("milis should be a positive value");
- return add(value, -1 * milis);
+ public static XMLGregorianCalendar subtract(XMLGregorianCalendar value, long millis) {
+ return add(value, - millis);
}
/**
@@ -106,7 +112,7 @@ public class XMLTimeUtil {
*
* @throws ConfigurationException
*/
- public static XMLGregorianCalendar getIssueInstant() throws ConfigurationException {
+ public static XMLGregorianCalendar getIssueInstant() {
return getIssueInstant(getCurrentTimeZoneID());
}
@@ -144,7 +150,7 @@ public class XMLTimeUtil {
* @return
*/
public static boolean isValid(XMLGregorianCalendar now, XMLGregorianCalendar notbefore, XMLGregorianCalendar notOnOrAfter) {
- int val = 0;
+ int val;
if (notbefore != null) {
val = notbefore.compare(now);
diff --git a/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java b/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java
index 916f7f3..7eebdad 100755
--- a/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java
+++ b/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java
@@ -18,7 +18,6 @@ package org.keycloak.saml;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
-import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
@@ -56,11 +55,7 @@ public class SAML2AuthnRequestBuilder implements SamlProtocolExtensionsAwareBuil
}
public SAML2AuthnRequestBuilder() {
- try {
- this.authnRequestType = new AuthnRequestType(IDGenerator.create("ID_"), XMLTimeUtil.getIssueInstant());
- } catch (ConfigurationException e) {
- throw new RuntimeException("Could not create SAML AuthnRequest builder.", e);
- }
+ this.authnRequestType = new AuthnRequestType(IDGenerator.create("ID_"), XMLTimeUtil.getIssueInstant());
}
public SAML2AuthnRequestBuilder assertionConsumerUrl(String assertionConsumerUrl) {
diff --git a/saml-core/src/main/java/org/keycloak/saml/validators/ConditionsValidator.java b/saml-core/src/main/java/org/keycloak/saml/validators/ConditionsValidator.java
new file mode 100644
index 0000000..d77a4fd
--- /dev/null
+++ b/saml-core/src/main/java/org/keycloak/saml/validators/ConditionsValidator.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2018 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.validators;
+
+import org.keycloak.dom.saml.common.CommonConditionsType;
+import org.keycloak.dom.saml.v2.assertion.AudienceRestrictionType;
+import org.keycloak.dom.saml.v2.assertion.ConditionAbstractType;
+import org.keycloak.dom.saml.v2.assertion.ConditionsType;
+import org.keycloak.dom.saml.v2.assertion.OneTimeUseType;
+import org.keycloak.dom.saml.v2.assertion.ProxyRestrictionType;
+import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import javax.xml.datatype.DatatypeConstants;
+import javax.xml.datatype.XMLGregorianCalendar;
+import org.jboss.logging.Logger;
+
+/**
+ * Conditions validation as per Section 2.5 of https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
+ * @author hmlnarik
+ */
+public class ConditionsValidator {
+
+ private static final Logger LOG = Logger.getLogger(ConditionsValidator.class);
+
+ public static enum Result {
+ VALID { @Override public Result joinResult(Result otherResult) { return otherResult; } },
+ INDETERMINATE { @Override public Result joinResult(Result otherResult) { return otherResult == INVALID ? INVALID : INDETERMINATE; } },
+ INVALID { @Override public Result joinResult(Result otherResult) { return INVALID; } };
+
+ /**
+ * Returns result as per Section 2.5.1.1
+ * @param otherResult
+ * @return
+ */
+ protected abstract Result joinResult(Result otherResult);
+ };
+
+ public static class Builder {
+
+ private final String assertionId;
+
+ private final CommonConditionsType conditions;
+
+ private final DestinationValidator destinationValidator;
+
+ private int clockSkewInMillis = 0;
+
+ private final Set<URI> allowedAudiences = new HashSet<>();
+
+ public Builder(String assertionId, CommonConditionsType conditions, DestinationValidator destinationValidator) {
+ this.assertionId = assertionId;
+ this.conditions = conditions;
+ this.destinationValidator = destinationValidator;
+ }
+
+ public Builder clockSkewInMillis(int clockSkewInMillis) {
+ this.clockSkewInMillis = clockSkewInMillis;
+ return this;
+ }
+
+ public Builder addAllowedAudience(URI... allowedAudiences) {
+ this.allowedAudiences.addAll(Arrays.asList(allowedAudiences));
+ return this;
+ }
+
+ public ConditionsValidator build() {
+ return new ConditionsValidator(assertionId, conditions, clockSkewInMillis, allowedAudiences, destinationValidator);
+ }
+
+ }
+
+ private final CommonConditionsType conditions;
+
+ private final int clockSkewInMillis;
+
+ private final String assertionId;
+
+ private final XMLGregorianCalendar now = XMLTimeUtil.getIssueInstant();
+
+ private final Set<URI> allowedAudiences;
+
+ private final DestinationValidator destinationValidator;
+
+ private int oneTimeConditionsCount = 0;
+
+ private int proxyRestrictionsCount = 0;
+
+ private ConditionsValidator(String assertionId, CommonConditionsType conditions, int clockSkewInMillis, Set<URI> allowedAudiences, DestinationValidator destinationValidator) {
+ this.assertionId = assertionId;
+ this.conditions = conditions;
+ this.clockSkewInMillis = clockSkewInMillis;
+ this.allowedAudiences = allowedAudiences;
+ this.destinationValidator = destinationValidator;
+ }
+
+ public boolean isValid() {
+ if (conditions == null) {
+ return true;
+ }
+
+ Result res = validateExpiration();
+ if (conditions instanceof ConditionsType) {
+ res = validateConditions((ConditionsType) conditions, res);
+ } else {
+ res = Result.INDETERMINATE;
+ LOG.infof("Unknown conditions in assertion %s: %s", assertionId, conditions == null ? "<null>" : conditions.getClass().getSimpleName());
+ }
+
+ LOG.debugf("Assertion %s validity is %s", assertionId, res.name());
+
+ return Result.VALID == res;
+ }
+
+ private Result validateConditions(ConditionsType ct, Result res) {
+ Iterator<ConditionAbstractType> it = ct.getConditions() == null
+ ? Collections.<ConditionAbstractType>emptySet().iterator()
+ : ct.getConditions().iterator();
+
+ while (it.hasNext() && res == Result.VALID) {
+ ConditionAbstractType cond = it.next();
+ Result r;
+ if (cond instanceof OneTimeUseType) {
+ r = validateOneTimeUse((OneTimeUseType) cond);
+ } else if (cond instanceof AudienceRestrictionType) {
+ r = validateAudienceRestriction((AudienceRestrictionType) cond);
+ } else if (cond instanceof ProxyRestrictionType) {
+ r = validateProxyRestriction((ProxyRestrictionType) cond);
+ } else {
+ r = Result.INDETERMINATE;
+ LOG.infof("Unknown condition in assertion %s: %s", assertionId, cond == null ? "<null>" : cond.getClass());
+ }
+
+ res = r.joinResult(res);
+ }
+
+ return res;
+ }
+
+ /**
+ * Validate as per Section 2.5.1.2
+ * @return
+ */
+ private Result validateExpiration() {
+ XMLGregorianCalendar notBefore = conditions.getNotBefore();
+ XMLGregorianCalendar notOnOrAfter = conditions.getNotOnOrAfter();
+
+ if (notBefore == null && notOnOrAfter == null) {
+ return Result.VALID;
+ }
+
+ if (notBefore != null && notOnOrAfter != null && notBefore.compare(notOnOrAfter) != DatatypeConstants.LESSER) {
+ return Result.INVALID;
+ }
+
+ XMLGregorianCalendar updatedNotBefore = XMLTimeUtil.subtract(notBefore, clockSkewInMillis);
+ XMLGregorianCalendar updatedOnOrAfter = XMLTimeUtil.add(notOnOrAfter, clockSkewInMillis);
+
+ LOG.debugf("Evaluating Conditions of Assertion %s. notBefore=%s, notOnOrAfter=%s", assertionId, notBefore, notOnOrAfter);
+ boolean valid = XMLTimeUtil.isValid(now, updatedNotBefore, updatedOnOrAfter);
+ if (! valid) {
+ LOG.infof("Assertion %s expired.", assertionId);
+ }
+
+ return valid ? Result.VALID : Result.INVALID;
+ }
+
+ /**
+ * Section 2.5.1.4
+ * @return
+ */
+ private Result validateAudienceRestriction(AudienceRestrictionType cond) {
+ for (URI aud : cond.getAudience()) {
+ for (URI allowedAudience : allowedAudiences) {
+ if (destinationValidator.validate(aud, allowedAudience)) {
+ return Result.VALID;
+ }
+ }
+ }
+
+ LOG.infof("Assertion %s is not addressed to this SP.", assertionId);
+ LOG.debugf("Allowed audiences are: %s", allowedAudiences);
+
+ return Result.INVALID;
+ }
+
+ /**
+ * Section 2.5.1.5
+ * @return
+ */
+ private Result validateOneTimeUse(OneTimeUseType cond) {
+ oneTimeConditionsCount++;
+
+ if (oneTimeConditionsCount > 1) { // line 960
+ LOG.info("Invalid conditions: Multiple <OneTimeUse/> conditions found.");
+ return Result.INVALID;
+ }
+
+ return Result.VALID; // See line 963 of spec
+ }
+
+ /**
+ * Section 2.5.1.6
+ * @return
+ */
+ private Result validateProxyRestriction(ProxyRestrictionType cond) {
+ proxyRestrictionsCount++;
+
+ if (proxyRestrictionsCount > 1) { // line 992
+ LOG.info("Invalid conditions: Multiple <ProxyRestriction/> conditions found.");
+ return Result.INVALID;
+ }
+
+ return Result.VALID; // See line 994 of spec
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlIdPInitiatedSsoTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlIdPInitiatedSsoTest.java
index 80c8ffd..a60be28 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlIdPInitiatedSsoTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlIdPInitiatedSsoTest.java
@@ -4,6 +4,8 @@ import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.common.util.StringPropertyReplacer;
+import org.keycloak.dom.saml.v2.assertion.AssertionType;
+import org.keycloak.dom.saml.v2.assertion.AudienceRestrictionType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
@@ -11,6 +13,7 @@ import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
+import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.pages.LoginPage;
@@ -24,6 +27,7 @@ import org.keycloak.testsuite.util.SamlClientBuilder;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Properties;
@@ -38,11 +42,16 @@ import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;
+import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
-import static org.keycloak.testsuite.broker.BrokerTestConstants.*;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_CONS_NAME;
+import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_PROV_NAME;
import static org.junit.Assert.assertThat;
/**
@@ -62,6 +71,10 @@ public class KcSamlIdPInitiatedSsoTest extends AbstractKeycloakTest {
@Page
protected UpdateAccountInformationPage updateAccountInformationPage;
+ private String urlRealmConsumer2;
+ private String urlRealmConsumer;
+ private String urlRealmProvider;
+
protected String getAuthRoot() {
return suiteContext.getAuthServerInfo().getContextRoot().toString();
}
@@ -86,14 +99,23 @@ public class KcSamlIdPInitiatedSsoTest extends AbstractKeycloakTest {
.forEach(Response::close);
}
+ @Before
+ public void initRealmUrls() {
+ urlRealmProvider = getAuthRoot() + "/auth/realms/" + REALM_PROV_NAME;
+ urlRealmConsumer = getAuthRoot() + "/auth/realms/" + REALM_CONS_NAME;
+ urlRealmConsumer2 = getAuthRoot() + "/auth/realms/" + REALM_CONS_NAME + "-2";
+ }
+
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
+ initRealmUrls();
+
Properties p = new Properties();
p.put("name.realm.provider", REALM_PROV_NAME);
p.put("name.realm.consumer", REALM_CONS_NAME);
- p.put("url.realm.provider", getAuthRoot() + "/auth/realms/" + REALM_PROV_NAME);
- p.put("url.realm.consumer", getAuthRoot() + "/auth/realms/" + REALM_CONS_NAME);
- p.put("url.realm.consumer-2", getAuthRoot() + "/auth/realms/" + REALM_CONS_NAME + "-2");
+ p.put("url.realm.provider", urlRealmProvider);
+ p.put("url.realm.consumer", urlRealmConsumer);
+ p.put("url.realm.consumer-2", urlRealmConsumer2);
testRealms.add(loadFromClasspath("kc3731-provider-realm.json", p));
testRealms.add(loadFromClasspath("kc3731-broker-realm.json", p));
@@ -153,8 +175,20 @@ public class KcSamlIdPInitiatedSsoTest extends AbstractKeycloakTest {
wait.until(condition);
}
+ private void assertAudience(ResponseType resp, String expectedAudience) throws Exception {
+ AssertionType a = AssertionUtil.getAssertion(null, resp, null);
+ assertThat(a, notNullValue());
+ assertThat(a.getConditions(), notNullValue());
+ assertThat(a.getConditions().getConditions(), notNullValue());
+ assertThat(a.getConditions().getConditions(), hasSize(greaterThan(0)));
+ assertThat(a.getConditions().getConditions().get(0), instanceOf(AudienceRestrictionType.class));
+
+ AudienceRestrictionType ar = (AudienceRestrictionType) a.getConditions().getConditions().get(0);
+ assertThat(ar.getAudience(), contains(URI.create(expectedAudience)));
+ }
+
@Test
- public void testProviderIdpInitiatedLoginToApp() {
+ public void testProviderIdpInitiatedLoginToApp() throws Exception {
SAMLDocumentHolder samlResponse = new SamlClientBuilder()
.navigateTo(getSamlIdpInitiatedUrl(REALM_PROV_NAME, "samlbroker"))
// Login in provider realm
@@ -166,6 +200,7 @@ public class KcSamlIdPInitiatedSsoTest extends AbstractKeycloakTest {
assertThat(ob, Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
ResponseType resp = (ResponseType) ob;
assertThat(resp.getDestination(), is(getSamlBrokerIdpInitiatedUrl(REALM_CONS_NAME, "sales")));
+ assertAudience(resp, getSamlBrokerIdpInitiatedUrl(REALM_CONS_NAME, "sales"));
return ob;
})
.build()
@@ -178,11 +213,12 @@ public class KcSamlIdPInitiatedSsoTest extends AbstractKeycloakTest {
assertThat(samlResponse.getSamlObject(), Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
ResponseType resp = (ResponseType) samlResponse.getSamlObject();
- assertThat(resp.getDestination(), is("http://localhost:8180/auth/realms/" + REALM_CONS_NAME + "/app/auth"));
+ assertThat(resp.getDestination(), is(urlRealmConsumer + "/app/auth"));
+ assertAudience(resp, urlRealmConsumer + "/app/auth");
}
@Test
- public void testConsumerIdpInitiatedLoginToApp() {
+ public void testConsumerIdpInitiatedLoginToApp() throws Exception {
SAMLDocumentHolder samlResponse = new SamlClientBuilder()
.navigateTo(getSamlIdpInitiatedUrl(REALM_CONS_NAME, "sales"))
// Request login via saml-leaf
@@ -201,6 +237,7 @@ public class KcSamlIdPInitiatedSsoTest extends AbstractKeycloakTest {
assertThat(ob, Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
ResponseType resp = (ResponseType) ob;
assertThat(resp.getDestination(), is(getSamlBrokerUrl(REALM_CONS_NAME)));
+ assertAudience(resp, urlRealmConsumer);
return ob;
})
.build()
@@ -213,11 +250,12 @@ public class KcSamlIdPInitiatedSsoTest extends AbstractKeycloakTest {
assertThat(samlResponse.getSamlObject(), Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
ResponseType resp = (ResponseType) samlResponse.getSamlObject();
- assertThat(resp.getDestination(), is("http://localhost:8180/auth/realms/" + REALM_CONS_NAME + "/app/auth"));
+ assertThat(resp.getDestination(), is(urlRealmConsumer + "/app/auth"));
+ assertAudience(resp, urlRealmConsumer + "/app/auth");
}
@Test
- public void testTwoConsequentIdpInitiatedLogins() {
+ public void testTwoConsequentIdpInitiatedLogins() throws Exception {
SAMLDocumentHolder samlResponse = new SamlClientBuilder()
.navigateTo(getSamlIdpInitiatedUrl(REALM_PROV_NAME, "samlbroker"))
// Login in provider realm
@@ -229,6 +267,7 @@ public class KcSamlIdPInitiatedSsoTest extends AbstractKeycloakTest {
assertThat(ob, Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
ResponseType resp = (ResponseType) ob;
assertThat(resp.getDestination(), is(getSamlBrokerIdpInitiatedUrl(REALM_CONS_NAME, "sales")));
+ assertAudience(resp, getSamlBrokerIdpInitiatedUrl(REALM_CONS_NAME, "sales"));
return ob;
})
.build()
@@ -241,12 +280,12 @@ public class KcSamlIdPInitiatedSsoTest extends AbstractKeycloakTest {
.transformObject(ob -> {
assertThat(ob, Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
ResponseType resp = (ResponseType) ob;
- assertThat(resp.getDestination(), is("http://localhost:8180/auth/realms/" + REALM_CONS_NAME + "/app/auth"));
+ assertThat(resp.getDestination(), is(urlRealmConsumer + "/app/auth"));
+ assertAudience(resp, urlRealmConsumer + "/app/auth");
return null;
})
.build()
-
// Now login to the second app
.navigateTo(getSamlIdpInitiatedUrl(REALM_PROV_NAME, "samlbroker-2"))
@@ -259,6 +298,7 @@ public class KcSamlIdPInitiatedSsoTest extends AbstractKeycloakTest {
assertThat(ob, Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
ResponseType resp = (ResponseType) ob;
assertThat(resp.getDestination(), is(getSamlBrokerIdpInitiatedUrl(REALM_CONS_NAME, "sales2")));
+ assertAudience(resp, getSamlBrokerIdpInitiatedUrl(REALM_CONS_NAME, "sales2"));
return ob;
})
.build()
@@ -267,16 +307,17 @@ public class KcSamlIdPInitiatedSsoTest extends AbstractKeycloakTest {
assertThat(samlResponse.getSamlObject(), Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
ResponseType resp = (ResponseType) samlResponse.getSamlObject();
- assertThat(resp.getDestination(), is("http://localhost:8180/auth/realms/" + REALM_CONS_NAME + "/app/auth/sales2/saml"));
+ assertThat(resp.getDestination(), is(urlRealmConsumer + "/app/auth2/saml"));
+ assertAudience(resp, urlRealmConsumer + "/app/auth2");
assertSingleUserSession(REALM_CONS_NAME, CONSUMER_CHOSEN_USERNAME,
- "http://localhost:8180/auth/realms/" + REALM_CONS_NAME + "/app/auth",
- "http://localhost:8180/auth/realms/" + REALM_CONS_NAME + "/app/auth2"
+ urlRealmConsumer + "/app/auth",
+ urlRealmConsumer + "/app/auth2"
);
assertSingleUserSession(REALM_PROV_NAME, PROVIDER_REALM_USER_NAME,
- getAuthRoot() + "/auth/realms/" + REALM_CONS_NAME,
- getAuthRoot() + "/auth/realms/" + REALM_CONS_NAME + "-2"
+ urlRealmConsumer + "/broker/saml-leaf/endpoint/clients/sales",
+ urlRealmConsumer + "/broker/saml-leaf/endpoint/clients/sales2"
);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-broker-realm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-broker-realm.json
index 0ed5ed3..76a68ad 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-broker-realm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-broker-realm.json
@@ -47,7 +47,7 @@
"saml.signature.algorithm": "RSA_SHA512",
"saml.signing.certificate": "MIIB1DCCAT0CBgFJGVacCDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1lbmMvMB4XDTE0MTAxNjE0MjA0NloXDTI0MTAxNjE0MjIyNlowMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3QtZW5jLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA2+5MCT5BnVN+IYnKZcH6ev1pjXGi4feE0nOycq/VJ3aeaZMi4G9AxOxCBPupErOC7Kgm/Bw5AdJyw+Q12wSRXfJ9FhqCrLXpb7YOhbVSTJ8De5O8mW35DxAlh/cxe9FXjqPb286wKTUZ3LfGYR+X235UQeCTAPS/Ufi21EXaEikCAwEAATANBgkqhkiG9w0BAQsFAAOBgQBMrfGD9QFfx5v7ld/OAto5rjkTe3R1Qei8XRXfcs83vLaqEzjEtTuLGrJEi55kXuJgBpVmQpnwCCkkjSy0JxbqLDdVi9arfWUxEGmOr01ZHycELhDNaQcFqVMPr5kRHIHgktT8hK2IgCvd3Fy9/JCgUgCPxKfhwecyEOKxUc857g==",
"saml.signing.private.key": "MIICXQIBAAKBgQDb7kwJPkGdU34hicplwfp6/WmNcaLh94TSc7Jyr9Undp5pkyLgb0DE7EIE+6kSs4LsqCb8HDkB0nLD5DXbBJFd8n0WGoKstelvtg6FtVJMnwN7k7yZbfkPECWH9zF70VeOo9vbzrApNRnct8ZhH5fbflRB4JMA9L9R+LbURdoSKQIDAQABAoGBANtbZG9bruoSGp2s5zhzLzd4hczT6Jfk3o9hYjzNb5Z60ymN3Z1omXtQAdEiiNHkRdNxK+EM7TcKBfmoJqcaeTkW8cksVEAW23ip8W9/XsLqmbU2mRrJiKa+KQNDSHqJi1VGyimi4DDApcaqRZcaKDFXg2KDr/Qt5JFD/o9IIIPZAkEA+ZENdBIlpbUfkJh6Ln+bUTss/FZ1FsrcPZWu13rChRMrsmXsfzu9kZUWdUeQ2Dj5AoW2Q7L/cqdGXS7Mm5XhcwJBAOGZq9axJY5YhKrsksvYRLhQbStmGu5LG75suF+rc/44sFq+aQM7+oeRr4VY88Mvz7mk4esdfnk7ae+cCazqJvMCQQCx1L1cZw3yfRSn6S6u8XjQMjWE/WpjulujeoRiwPPY9WcesOgLZZtYIH8nRL6ehEJTnMnahbLmlPFbttxPRUanAkA11MtSIVcKzkhp2KV2ipZrPJWwI18NuVJXb+3WtjypTrGWFZVNNkSjkLnHIeCYlJIGhDd8OL9zAiBXEm6kmgLNAkBWAg0tK2hCjvzsaA505gWQb4X56uKWdb0IzN+fOLB3Qt7+fLqbVQNQoNGzqey6B4MoS1fUKAStqdGTFYPG/+9t",
- "saml_assertion_consumer_url_post" : "http://localhost:8180/auth/realms/${name.realm.consumer}/app/auth/sales2/saml",
+ "saml_assertion_consumer_url_post" : "http://localhost:8180/auth/realms/${name.realm.consumer}/app/auth2/saml",
"saml_idp_initiated_sso_url_name" : "sales2"
},
"baseUrl": "http://localhost:8180/auth/realms/${name.realm.consumer}/app/auth2",
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-provider-realm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-provider-realm.json
index d660118..82f4f18 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-provider-realm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-provider-realm.json
@@ -27,12 +27,32 @@
"saml.server.signature" : "false",
"saml_assertion_consumer_url_post" : "${url.realm.consumer}/broker/saml-leaf/endpoint/clients/sales",
"saml_force_name_id_format" : "false",
- "saml_idp_initiated_sso_url_name" : "samlbroker",
"saml_name_id_format": "email",
"saml_single_logout_service_url_post" : "${url.realm.consumer}/broker/saml-leaf/endpoint"
}
}, {
- "clientId": "${url.realm.consumer-2}",
+ "clientId": "${url.realm.consumer}/broker/saml-leaf/endpoint/clients/sales",
+ "enabled": true,
+ "protocol": "saml",
+ "fullScopeAllowed": true,
+ "redirectUris": [
+ "${url.realm.consumer}/broker/saml-leaf/endpoint"
+ ],
+ "attributes" : {
+ "saml_name_id_format": "email",
+ "saml.assertion.signature" : "false",
+ "saml.authnstatement" : "true",
+ "saml.client.signature" : "false",
+ "saml.encrypt" : "false",
+ "saml.force.post.binding" : "true",
+ "saml.server.signature" : "false",
+ "saml_assertion_consumer_url_post" : "${url.realm.consumer}/broker/saml-leaf/endpoint/clients/sales",
+ "saml_force_name_id_format" : "false",
+ "saml_idp_initiated_sso_url_name" : "samlbroker",
+ "saml_single_logout_service_url_post" : "${url.realm.consumer}/broker/saml-leaf/endpoint"
+ }
+ }, {
+ "clientId": "${url.realm.consumer}/broker/saml-leaf/endpoint/clients/sales2",
"enabled": true,
"protocol": "saml",
"fullScopeAllowed": true,