diff --git a/module.xml b/module.xml
index f75aebd..86fc14c 100644
--- a/module.xml
+++ b/module.xml
@@ -7,6 +7,7 @@
+
@@ -20,6 +21,6 @@
-
+
-
\ No newline at end of file
+
diff --git a/pom.xml b/pom.xml
index 0d3f7d8..fd5b18b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,7 +40,7 @@
- 3.3.2
+ 2.6
1.10.19
@@ -87,8 +87,8 @@
- org.apache.commons
- commons-lang3
+ commons-lang
+ commons-lang
${apache.commons.lang}
provided
diff --git a/src/main/java/com/quest/keycloak/broker/wsfed/RequestedToken.java b/src/main/java/com/quest/keycloak/broker/wsfed/RequestedToken.java
index a58c505..9550377 100644
--- a/src/main/java/com/quest/keycloak/broker/wsfed/RequestedToken.java
+++ b/src/main/java/com/quest/keycloak/broker/wsfed/RequestedToken.java
@@ -18,8 +18,25 @@
import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.saml.common.exceptions.ProcessingException;
+import org.keycloak.saml.processing.core.util.JAXPValidationUtil;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
import javax.ws.rs.core.Response;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+import java.io.IOException;
+import java.io.StringReader;
import java.security.PublicKey;
public interface RequestedToken {
@@ -34,4 +51,46 @@ public interface RequestedToken {
String getSessionIndex();
Object getToken();
+
+ String getFirstName();
+
+ String getLastName();
+
+ default Document createXmlDocument(String response) throws ProcessingException, ParserConfigurationException {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+
+ InputSource source = new InputSource();
+ source.setCharacterStream(new StringReader(response));
+ try {
+ Document document = builder.parse(source);
+ JAXPValidationUtil.checkSchemaValidation(document);
+ return document;
+ } catch (SAXException | IOException e) {
+ throw new ProcessingException("Error while extracting SAML from WSFed response.");
+ }
+ }
+
+ default Document extractSamlDocument(Document document) throws ProcessingException, XPathExpressionException {
+ try {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ XPath xpath = XPathFactory.newInstance().newXPath();
+ XPathExpression xPathExpression = xpath.compile("//*[local-name() = 'Assertion']");
+
+ NodeList samlNodes = (NodeList) xPathExpression.evaluate(document, XPathConstants.NODESET);
+ Document samlDoc = factory.newDocumentBuilder().newDocument();
+ for (int i = 0; i < samlNodes.getLength(); i++) {
+ Node node = samlNodes.item(i);
+ Node copyNode = samlDoc.importNode(node, true);
+ samlDoc.appendChild(copyNode);
+ }
+ return samlDoc;
+ } catch (XPathExpressionException | ParserConfigurationException e) {
+ throw new ProcessingException("Error while extracting SAML Assertion from WSFed XML document.");
+ }
+ }
+
+
}
diff --git a/src/main/java/com/quest/keycloak/broker/wsfed/SAML11RequestedToken.java b/src/main/java/com/quest/keycloak/broker/wsfed/SAML11RequestedToken.java
new file mode 100644
index 0000000..45fccf5
--- /dev/null
+++ b/src/main/java/com/quest/keycloak/broker/wsfed/SAML11RequestedToken.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2016 Analytical Graphics, 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 com.quest.keycloak.broker.wsfed;
+
+import com.quest.keycloak.protocol.wsfed.sig.SAML11Signature;
+import org.jboss.logging.Logger;
+import org.keycloak.dom.saml.v1.assertion.SAML11AssertionType;
+import org.keycloak.dom.saml.v1.assertion.SAML11AttributeStatementType;
+import org.keycloak.dom.saml.v1.assertion.SAML11AttributeType;
+import org.keycloak.dom.saml.v1.assertion.SAML11AudienceRestrictionCondition;
+import org.keycloak.dom.saml.v1.assertion.SAML11ConditionAbstractType;
+import org.keycloak.dom.saml.v1.assertion.SAML11ConditionsType;
+import org.keycloak.dom.saml.v1.assertion.SAML11StatementAbstractType;
+import org.keycloak.dom.saml.v1.assertion.SAML11SubjectType;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+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.core.parsers.saml.SAMLParser;
+import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
+import org.keycloak.services.ErrorPage;
+import org.keycloak.services.messages.Messages;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import javax.ws.rs.core.Response;
+import javax.xml.datatype.DatatypeFactory;
+import javax.xml.datatype.XMLGregorianCalendar;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.security.PublicKey;
+import java.util.List;
+
+/**
+ * @author Peter Nalyvayko
+ * @version $Revision: 1 $
+ * @date 10/4/2016
+ */
+
+public class SAML11RequestedToken implements RequestedToken {
+
+ // private NameIDType subjectNameID;
+ protected static final Logger logger = Logger.getLogger(SAML2RequestedToken.class);
+ private SAML11AssertionType samlAssertion;
+ private String wsfedResponse;
+
+ public SAML11RequestedToken(String wsfedResponse, Object token, RealmModel realm) throws IOException, ParsingException, ProcessingException, ConfigurationException {
+ this.wsfedResponse = wsfedResponse;
+ this.samlAssertion = getAssertionType(token, realm);
+ }
+
+ public static boolean isSignatureValid(Element assertionElement, PublicKey publicKey) {
+ try {
+ Document doc = DocumentUtil.createDocument();
+ Node n = doc.importNode(assertionElement, true);
+ doc.appendChild(n);
+
+ return new SAML11Signature().validate(doc, publicKey);
+ } catch (Exception e) {
+ logger.error("Cannot validate signature of assertion", e);
+ }
+ return false;
+ }
+
+ @Override
+ public Response validate(PublicKey key, WSFedIdentityProviderConfig config, EventBuilder event, KeycloakSession session) {
+ try {
+ //We have to use the wsfedResponse and pull the document from it. The reason is the WSTrustParser sometimes re-organizes some attributes within the RequestedSecurityToken which breaks validation.
+ Document doc = createXmlDocument(wsfedResponse);
+ if(!isSignatureValid(extractSamlDocument(doc).getDocumentElement(), key)) {
+ event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
+ event.error(Errors.INVALID_SIGNATURE);
+ return ErrorPage.error(session, Messages.INVALID_FEDERATED_IDENTITY_ACTION);
+ }
+
+ XMLGregorianCalendar notBefore = samlAssertion.getConditions().getNotBefore();
+ //Add in a tiny bit of slop for small clock differences
+ notBefore.add(DatatypeFactory.newInstance().newDuration(false, 0, 0, 0, 0, 0, 10));
+
+ if(AssertionUtil.hasExpired(samlAssertion)) {
+ event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
+ event.error(Errors.EXPIRED_CODE);
+ return ErrorPage.error(session, Messages.INVALID_FEDERATED_IDENTITY_ACTION);
+ }
+
+ if(!isValidAudienceRestriction(URI.create(config.getWsFedRealm()))) {
+ event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
+ event.error(Errors.INVALID_SAML_RESPONSE);
+ return ErrorPage.error(session, Messages.INVALID_FEDERATED_IDENTITY_ACTION);
+ }
+
+ } catch (Exception e) {
+ logger.error("Unable to validate signature", e);
+ event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
+ event.error(Errors.INVALID_SAML_RESPONSE);
+ return ErrorPage.error(session, Messages.INVALID_FEDERATED_IDENTITY_ACTION);
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getFirstName() {
+ if (!samlAssertion.getStatements().isEmpty()) {
+ for (SAML11StatementAbstractType st : samlAssertion.getStatements()) {
+ if (st instanceof SAML11AttributeStatementType) {
+ SAML11AttributeStatementType attributeStatement = (SAML11AttributeStatementType)st;
+ for (SAML11AttributeType attribute : attributeStatement.get()) {
+ if ("givenname".equals(attribute.getAttributeName())
+ || JBossSAMLURIConstants.CLAIMS_GIVEN_NAME.get().equalsIgnoreCase(attribute.getAttributeName())) {
+ if (!attribute.get().isEmpty()) {
+ return attribute.get().get(0).toString();
+ }
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getLastName() {
+ if (!samlAssertion.getStatements().isEmpty()) {
+ for (SAML11StatementAbstractType st : samlAssertion.getStatements()) {
+ if (st instanceof SAML11AttributeStatementType) {
+ SAML11AttributeStatementType attributeStatement = (SAML11AttributeStatementType)st;
+ for (SAML11AttributeType attribute : attributeStatement.get()) {
+ if ("surname".equals(attribute.getAttributeName())
+ || JBossSAMLURIConstants.CLAIMS_SURNAME.get().equalsIgnoreCase(attribute.getAttributeName())) {
+ if (!attribute.get().isEmpty()) {
+ return attribute.get().get(0).toString();
+ }
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getUsername() {
+ if (!samlAssertion.getStatements().isEmpty()) {
+ for (SAML11StatementAbstractType st : samlAssertion.getStatements()) {
+ if (st instanceof SAML11AttributeStatementType) {
+ SAML11AttributeStatementType attributeStatement = (SAML11AttributeStatementType)st;
+ // The "name" claim is a username
+ for (SAML11AttributeType attribute : attributeStatement.get()) {
+ if ("name".equalsIgnoreCase(attribute.getAttributeName())
+ || JBossSAMLURIConstants.CLAIMS_NAME.get().equalsIgnoreCase(attribute.getAttributeName())) {
+ if (!attribute.get().isEmpty()) {
+ return attribute.get().get(0).toString();
+ }
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getEmail() {
+ if (!samlAssertion.getStatements().isEmpty()) {
+ for (SAML11StatementAbstractType st : samlAssertion.getStatements()) {
+ if (st instanceof SAML11AttributeStatementType) {
+ SAML11AttributeStatementType attributeStatement = (SAML11AttributeStatementType)st;
+ for (SAML11AttributeType attribute : attributeStatement.get()) {
+ if ("emailaddress".equalsIgnoreCase(attribute.getAttributeName())
+ || JBossSAMLURIConstants.CLAIMS_EMAIL_ADDRESS_2005.get().equals(attribute.getAttributeName())) {
+ if (!attribute.get().isEmpty()) {
+ return attribute.get().get(0).toString();
+ }
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getId() {
+ if (!samlAssertion.getStatements().isEmpty()) {
+ for (SAML11StatementAbstractType st : samlAssertion.getStatements()) {
+ if (st instanceof SAML11AttributeStatementType) {
+ SAML11AttributeStatementType attributeStatement = (SAML11AttributeStatementType)st;
+ // First check if the nameIdentifier of the Subject is available
+ // Can the subject be false? What does the spec say?
+ SAML11SubjectType subject = attributeStatement.getSubject();
+ if (subject != null && subject.getChoice() != null) {
+ SAML11SubjectType.SAML11SubjectTypeChoice choice = subject.getChoice();
+ if (choice.getNameID() != null) {
+ String nameId = choice.getNameID().getValue();
+ if (nameId != null && nameId.length() > 0) {
+ return nameId;
+ }
+ }
+ }
+ // The "nameidentifier" is a unique user id.
+ for (SAML11AttributeType attribute : attributeStatement.get()) {
+ if ("nameidenfier".equalsIgnoreCase(attribute.getAttributeName())
+ || JBossSAMLURIConstants.CLAIMS_NAME_IDENTIFIER.get().equalsIgnoreCase(attribute.getAttributeName())) {
+ if (!attribute.get().isEmpty()) {
+ return attribute.get().get(0).toString();
+ }
+ }
+ }
+ }
+ }
+ }
+ return getUsername();
+ }
+
+ @Override
+ public String getSessionIndex() {
+ //TODO: getSessionIndex still needs to be implemented
+ return null;
+ }
+
+ @Override
+ public Object getToken() { return samlAssertion; }
+
+ public boolean isValidAudienceRestriction(URI...uris) {
+ List audienceRestriction = getAudienceRestrictions();
+
+ if(audienceRestriction == null) {
+ return true;
+ }
+
+ for (URI uri : uris) {
+ if (audienceRestriction.contains(uri)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public List getAudienceRestrictions() {
+ SAML11ConditionsType conditions = samlAssertion.getConditions();
+ for(SAML11ConditionAbstractType condition : conditions.get()) {
+ if(condition instanceof SAML11AudienceRestrictionCondition) {
+ return ((SAML11AudienceRestrictionCondition) condition).get();
+ }
+ }
+
+ return null;
+ }
+
+ public SAML11AssertionType getAssertionType(Object token, RealmModel realm) throws IOException, ParsingException, ProcessingException, ConfigurationException {
+ SAML11AssertionType assertionType = null;
+ ByteArrayInputStream bis = null;
+ try {
+ String assertionXml = DocumentUtil.asString(((Element) token).getOwnerDocument());
+
+ bis = new ByteArrayInputStream(assertionXml.getBytes());
+ SAMLParser parser = new SAMLParser();
+ Object assertion = parser.parse(bis);
+
+ assertionType = (SAML11AssertionType) assertion;
+ return assertionType;
+ } finally {
+ if (bis != null) {
+ bis.close();
+ }
+ }
+ }
+
+ public SAML11AssertionType getAssertionType() {
+ return samlAssertion;
+ }
+}
diff --git a/src/main/java/com/quest/keycloak/broker/wsfed/SAML2RequestedToken.java b/src/main/java/com/quest/keycloak/broker/wsfed/SAML2RequestedToken.java
index 263bf82..47ca249 100644
--- a/src/main/java/com/quest/keycloak/broker/wsfed/SAML2RequestedToken.java
+++ b/src/main/java/com/quest/keycloak/broker/wsfed/SAML2RequestedToken.java
@@ -17,7 +17,14 @@
package com.quest.keycloak.broker.wsfed;
import org.jboss.logging.Logger;
-import org.keycloak.dom.saml.v2.assertion.*;
+import org.keycloak.dom.saml.v2.assertion.AssertionType;
+import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
+import org.keycloak.dom.saml.v2.assertion.AttributeType;
+import org.keycloak.dom.saml.v2.assertion.AudienceRestrictionType;
+import org.keycloak.dom.saml.v2.assertion.ConditionAbstractType;
+import org.keycloak.dom.saml.v2.assertion.EncryptedAssertionType;
+import org.keycloak.dom.saml.v2.assertion.NameIDType;
+import org.keycloak.dom.saml.v2.assertion.SubjectType;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
@@ -41,21 +48,13 @@
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
import javax.ws.rs.core.Response;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.xpath.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
-import java.io.StringReader;
import java.net.URI;
import java.security.PrivateKey;
import java.security.PublicKey;
@@ -143,43 +142,6 @@ public List getAudienceRestrictions() {
return null;
}
- private static Document createXmlDocument(String response) throws ProcessingException, ParserConfigurationException {
- DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
- factory.setNamespaceAware(true);
- DocumentBuilder builder = null;
-
- builder = factory.newDocumentBuilder();
- InputSource source = new InputSource();
- source.setCharacterStream(new StringReader(response));
- try {
- Document document = builder.parse(source);
- JAXPValidationUtil.checkSchemaValidation(document);
- return document;
- } catch (SAXException | IOException e) {
- throw new ProcessingException("Error while extracting SAML from WSFed response.");
- }
- }
-
- private Document extractSamlDocument(Document document) throws ProcessingException {
- try {
- DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
- factory.setNamespaceAware(true);
- XPath xpath = XPathFactory.newInstance().newXPath();
- XPathExpression xPathExpression = xpath.compile("//*[local-name() = 'Assertion']");
-
- NodeList samlNodes = (NodeList) xPathExpression.evaluate(document, XPathConstants.NODESET);
- Document samlDoc = factory.newDocumentBuilder().newDocument();
- for (int i = 0; i < samlNodes.getLength(); i++) {
- Node node = samlNodes.item(i);
- Node copyNode = samlDoc.importNode(node, true);
- samlDoc.appendChild(copyNode);
- }
- return samlDoc;
- } catch (XPathExpressionException | ParserConfigurationException e) {
- throw new ProcessingException("Error while extracting SAML Assertion from WSFed XML document.");
- }
- }
-
protected NameIDType getSubjectNameID() {
if (subjectNameID == null) {
SubjectType subject = saml2Assertion.getSubject();
@@ -197,6 +159,18 @@ public String getUsername() {
return getId();
}
+ @Override
+ public String getLastName() {
+ // TODO: implement getLastName
+ return null;
+ }
+
+ @Override
+ public String getFirstName() {
+ // TODO: implement getFirstName
+ return null;
+ }
+
@Override
public String getEmail() {
if (getSubjectNameID()!=null && getSubjectNameID().getFormat()!=null) {
@@ -286,7 +260,6 @@ public AssertionType getAssertionType() {
return saml2Assertion;
}
- public Object getToken() {
- return saml2Assertion;
- }
+ @Override
+ public Object getToken() { return saml2Assertion; }
}
diff --git a/src/main/java/com/quest/keycloak/broker/wsfed/WSFedDataMarshaller.java b/src/main/java/com/quest/keycloak/broker/wsfed/WSFedDataMarshaller.java
index ce4ca74..dbf006f 100644
--- a/src/main/java/com/quest/keycloak/broker/wsfed/WSFedDataMarshaller.java
+++ b/src/main/java/com/quest/keycloak/broker/wsfed/WSFedDataMarshaller.java
@@ -17,11 +17,13 @@
package com.quest.keycloak.broker.wsfed;
import org.keycloak.broker.provider.DefaultDataMarshaller;
+import org.keycloak.dom.saml.v1.assertion.SAML11AssertionType;
import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.StaxUtil;
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
+import org.keycloak.saml.processing.core.saml.v1.writers.SAML11AssertionWriter;
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLAssertionWriter;
import java.io.ByteArrayInputStream;
@@ -42,7 +44,18 @@ public String serialize(Object obj) {
} catch (ProcessingException pe) {
throw new RuntimeException(pe);
}
+ }
+ else if (obj instanceof SAML11AssertionType) {
+ try {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ SAML11AssertionType assertion = (SAML11AssertionType) obj;
+ SAML11AssertionWriter samlWriter = new SAML11AssertionWriter(StaxUtil.getXMLStreamWriter(bos));
+ samlWriter.write(assertion);
+ return new String(bos.toByteArray());
+ } catch (ProcessingException pe) {
+ throw new RuntimeException(pe);
+ }
}
//else if (obj instanceof JWSInput
else {
@@ -63,6 +76,17 @@ public T deserialize(String serialized, Class clazz) {
throw new RuntimeException(pe);
}
}
+ else if (clazz.equals(SAML11AssertionType.class)) {
+ try {
+ byte[] bytes = serialized.getBytes();
+ InputStream is = new ByteArrayInputStream(bytes);
+ Object respType = new SAMLParser().parse(is);
+
+ return clazz.cast(respType);
+ } catch (ParsingException pe) {
+ throw new RuntimeException(pe);
+ }
+ }
//else if(clazz.equals(JWSInput.class))
else {
return super.deserialize(serialized, clazz);
diff --git a/src/main/java/com/quest/keycloak/broker/wsfed/WSFedEndpoint.java b/src/main/java/com/quest/keycloak/broker/wsfed/WSFedEndpoint.java
index 57832ff..21de88c 100644
--- a/src/main/java/com/quest/keycloak/broker/wsfed/WSFedEndpoint.java
+++ b/src/main/java/com/quest/keycloak/broker/wsfed/WSFedEndpoint.java
@@ -279,6 +279,16 @@ protected Response handleLoginResponse(String wsfedResponse, RequestedToken toke
identity.setEmail(token.getEmail());
}
+ String givenName;
+ if ((givenName = token.getFirstName()) != null) {
+ identity.setFirstName(givenName);
+ }
+
+ String surName;
+ if ((surName = token.getLastName()) != null) {
+ identity.setLastName(surName);
+ }
+
if (config.isStoreToken()) {
identity.setToken(wsfedResponse);
}
@@ -316,7 +326,7 @@ protected Response handleWsFedResponse(String wsfedResponse, String context) {
token = new SAML2RequestedToken(session, wsfedResponse, rt, realm);
}
else if (rstr.getTokenType().compareTo(URI.create("urn:oasis:names:tc:SAML:1.0:assertion")) == 0) {
- throw new NotImplementedException("We don't currently support a token type of urn:oasis:names:tc:SAML:1.0:assertion");
+ token = new SAML11RequestedToken(wsfedResponse, rt, realm);
}
else if (rstr.getTokenType().compareTo(URI.create("urn:ietf:params:oauth:token-type:jwt")) == 0) {
throw new NotImplementedException("We don't currently support a token type of urn:ietf:params:oauth:token-type:jwt");
diff --git a/src/main/java/com/quest/keycloak/protocol/wsfed/WSFedLoginProtocol.java b/src/main/java/com/quest/keycloak/protocol/wsfed/WSFedLoginProtocol.java
index 4442ef4..11bbe84 100644
--- a/src/main/java/com/quest/keycloak/protocol/wsfed/WSFedLoginProtocol.java
+++ b/src/main/java/com/quest/keycloak/protocol/wsfed/WSFedLoginProtocol.java
@@ -16,19 +16,10 @@
package com.quest.keycloak.protocol.wsfed;
-import java.io.InputStream;
-import java.security.KeyPair;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.cert.X509Certificate;
-import javax.ws.rs.HttpMethod;
-import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriInfo;
-import javax.xml.datatype.DatatypeConfigurationException;
-
import com.quest.keycloak.common.wsfed.WSFedConstants;
import com.quest.keycloak.common.wsfed.builders.WSFedResponseBuilder;
+import com.quest.keycloak.protocol.wsfed.builders.RequestSecurityTokenResponseBuilder;
+import com.quest.keycloak.protocol.wsfed.builders.WSFedOIDCAccessTokenBuilder;
import com.quest.keycloak.protocol.wsfed.builders.WSFedSAML2AssertionTypeBuilder;
import com.quest.keycloak.protocol.wsfed.builders.WsFedSAML11AssertionTypeBuilder;
import org.apache.http.HttpEntity;
@@ -55,8 +46,13 @@
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
-import com.quest.keycloak.protocol.wsfed.builders.RequestSecurityTokenResponseBuilder;
-import com.quest.keycloak.protocol.wsfed.builders.WSFedOIDCAccessTokenBuilder;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.datatype.DatatypeConfigurationException;
+import java.io.InputStream;
+import java.security.KeyPair;
/**
* Implementation of keycloak's LoginProtocol. This is necessary
diff --git a/src/main/java/com/quest/keycloak/protocol/wsfed/builders/RequestSecurityTokenResponseBuilder.java b/src/main/java/com/quest/keycloak/protocol/wsfed/builders/RequestSecurityTokenResponseBuilder.java
index 9b3e409..7048438 100644
--- a/src/main/java/com/quest/keycloak/protocol/wsfed/builders/RequestSecurityTokenResponseBuilder.java
+++ b/src/main/java/com/quest/keycloak/protocol/wsfed/builders/RequestSecurityTokenResponseBuilder.java
@@ -21,7 +21,7 @@
import com.quest.keycloak.protocol.wsfed.sig.SAML11Signature;
import com.quest.keycloak.protocol.wsfed.sig.SAML2SignatureProxy;
import com.quest.keycloak.protocol.wsfed.sig.SAMLAbstractSignature;
-import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.commons.lang.StringEscapeUtils;
import org.keycloak.dom.saml.v1.assertion.SAML11AssertionType;
import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.saml.SignatureAlgorithm;
@@ -183,7 +183,7 @@ public Response buildResponse() throws ProcessingException, org.picketlink.commo
public RequestSecurityTokenResponse build() throws ConfigurationException, ProcessingException {
RequestSecurityTokenResponse response = new RequestSecurityTokenResponse();
- response.setContext(StringEscapeUtils.escapeXml11(context));
+ response.setContext(StringEscapeUtils.escapeXml(context));
XMLGregorianCalendar issueInstance = XMLTimeUtil.getIssueInstant();
response.setLifetime(new Lifetime(issueInstance.toGregorianCalendar(), XMLTimeUtil.add(issueInstance, tokenExpiration * 1000).toGregorianCalendar()));
diff --git a/src/main/java/com/quest/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java b/src/main/java/com/quest/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
index 6b25d6c..a7b1f2a 100644
--- a/src/main/java/com/quest/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
+++ b/src/main/java/com/quest/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
@@ -1,19 +1,14 @@
package com.quest.keycloak.saml.processing.core.saml.v2.util;
import org.keycloak.dom.saml.v1.assertion.SAML11AssertionType;
-import org.keycloak.dom.saml.v1.assertion.SAML11AttributeStatementType;
-import org.keycloak.dom.saml.v1.assertion.SAML11AttributeType;
-import org.keycloak.dom.saml.v1.assertion.SAML11ConditionsType;
-import org.keycloak.dom.saml.v1.assertion.SAML11StatementAbstractType;
-import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.PicketLinkLogger;
import org.keycloak.saml.common.PicketLinkLoggerFactory;
+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.core.saml.v1.writers.SAML11AssertionWriter;
import org.w3c.dom.Document;
-import javax.xml.datatype.XMLGregorianCalendar;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
diff --git a/src/test/java/com/quest/keycloak/common/wsfed/MockHelper.java b/src/test/java/com/quest/keycloak/common/wsfed/MockHelper.java
index 5fd2f68..a2295de 100644
--- a/src/test/java/com/quest/keycloak/common/wsfed/MockHelper.java
+++ b/src/test/java/com/quest/keycloak/common/wsfed/MockHelper.java
@@ -75,6 +75,7 @@ public class MockHelper {
private String realmName = null;
private int accessCodeLifespan = 0;
private int accessTokenLifespan = 0;
+ private int accessTokenLifespanForImplicitFlow = 0;
private Map protocolMappers = new HashMap<>();
@@ -352,6 +353,15 @@ public MockHelper setAccessTokenLifespan(int accessTokenLifespan) {
return this;
}
+ public int getAccessTokenLifespanForImplicitFlow() {
+ return accessTokenLifespanForImplicitFlow;
+ }
+
+ public MockHelper setAccessTokenLifespanForExplicitFlow(int accessTokenLifespanForExplicitFlow) {
+ this.accessTokenLifespanForImplicitFlow = accessTokenLifespanForExplicitFlow;
+ return this;
+ }
+
public UriInfo getUriInfo() {
return uriInfo;
}
diff --git a/src/test/java/com/quest/keycloak/protocol/wsfed/WSFedLoginProtocolTest.java b/src/test/java/com/quest/keycloak/protocol/wsfed/WSFedLoginProtocolTest.java
index c93c2b1..7f0ef57 100644
--- a/src/test/java/com/quest/keycloak/protocol/wsfed/WSFedLoginProtocolTest.java
+++ b/src/test/java/com/quest/keycloak/protocol/wsfed/WSFedLoginProtocolTest.java
@@ -31,6 +31,7 @@
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.LoginProtocol;
+import org.keycloak.saml.processing.core.saml.v1.SAML11Constants;
import org.keycloak.services.messages.Messages;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -112,6 +113,22 @@ public void testDontUseJwt() throws Exception {
assertFalse(loginProtocol.useJwt(client));
}
+ @Test
+ public void testSaml20AssertionTokenFormat() throws Exception {
+ ClientModel client = mockHelper.getClient();
+ doReturn("SAML 2.0").when(client).getAttribute(WSFedLoginProtocol.WSFED_SAML_ASSERTION_TOKEN_FORMAT);
+
+ assertEquals(WsFedSAMLAssertionTokenFormat.SAML20_ASSERTION_TOKEN_FORMAT, loginProtocol.getSamlAssertionTokenFormat(client));
+ }
+
+ @Test
+ public void testSaml11AssertionTokenFormat() throws Exception {
+ ClientModel client = mockHelper.getClient();
+ doReturn("SAML 1.1").when(client).getAttribute(WSFedLoginProtocol.WSFED_SAML_ASSERTION_TOKEN_FORMAT);
+
+ assertEquals(WsFedSAMLAssertionTokenFormat.SAML11_ASSERTION_TOKEN_FORMAT, loginProtocol.getSamlAssertionTokenFormat(client));
+ }
+
@Test
public void testIncludeX5t() throws Exception {
ClientModel client = mockHelper.getClient();
@@ -152,6 +169,31 @@ public void testAuthenticatedSaml() throws Exception {
assertTokenType(wsfedResponse, "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0");
}
+ @Test
+ public void testAuthenticatedSaml11() throws Exception {
+ ClientModel client = mockHelper.getClient();
+ doReturn("false").when(client).getAttribute(WSFedLoginProtocol.WSFED_JWT);
+ doReturn("SAML 1.1").when(client).getAttribute(WSFedLoginProtocol.WSFED_SAML_ASSERTION_TOKEN_FORMAT);
+
+ Response response = loginProtocol.authenticated(mockHelper.getUserSessionModel(), mockHelper.getAccessCode());
+
+ //We already validate token generation through other test classes so this is mainly to ensure the response gets built correctly
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+ assertEquals(MediaType.TEXT_HTML_TYPE, response.getMetadata().getFirst("Content-Type"));
+ assertEquals("no-cache", response.getMetadata().getFirst("Pragma"));
+ assertEquals("no-cache, no-store", response.getMetadata().getFirst("Cache-Control"));
+
+ Document doc = responseToDocument(response);
+
+ assertFormAction(doc, "POST", mockHelper.getClientSessionModel().getRedirectUri());
+ assertInputNode(doc, WSFedConstants.WSFED_ACTION, WSFedConstants.WSFED_SIGNIN_ACTION);
+ assertInputNode(doc, WSFedConstants.WSFED_REALM, client.getClientId());
+
+ String wsfedResponse = getInputNodeValue(doc, WSFedConstants.WSFED_RESULT);
+ assertNotNull(wsfedResponse);
+ assertTokenType(wsfedResponse, SAML11Constants.ASSERTION_11_NSURI);
+ }
+
@Test
public void testAuthenticatedJwt() throws Exception {
ClientModel client = mockHelper.getClient();
diff --git a/src/test/java/com/quest/keycloak/protocol/wsfed/WSFedServiceTest.java b/src/test/java/com/quest/keycloak/protocol/wsfed/WSFedServiceTest.java
index bfff6e6..e746c1a 100644
--- a/src/test/java/com/quest/keycloak/protocol/wsfed/WSFedServiceTest.java
+++ b/src/test/java/com/quest/keycloak/protocol/wsfed/WSFedServiceTest.java
@@ -24,7 +24,9 @@
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
@@ -41,6 +43,7 @@
import org.keycloak.services.resources.RealmsResource;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.mockito.internal.matchers.EndsWith;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
@@ -66,6 +69,7 @@ public class WSFedServiceTest {
@Mock private HttpRequest request;
@Mock private HttpResponse response;
@Mock private ClientConnection clientConnection;
+ @Mock private IdentityProviderModel identityProvider;
private MockHelper mockHelper;
private WrappedWSFedService service;
@@ -82,6 +86,11 @@ public void setUp() throws Exception {
injectMocks(service);
}
+ @After
+ public void tearDown() throws Exception {
+ ResteasyProviderFactory.clearContextData();
+ }
+
protected class WrappedWSFedService extends WSFedService {
public WrappedWSFedService(RealmModel realm, EventBuilder event) {
super(realm, event);
@@ -576,4 +585,41 @@ public void testHandleLoginRequest() throws Exception {
assertErrorPage(mockHelper.getLoginFormsProvider(), Messages.INVALID_CODE);
}
+ @Test
+ @Ignore
+ public void testHandleLoginRequestRedirectToIdentityProvider() throws Exception {
+ WSFedProtocolParameters params = new WSFedProtocolParameters();
+ params.setWsfed_reply("https://redirectUri");
+ params.setWsfed_context("context");
+ // The home realm parameter 'whr' is used to log in using an identity provider
+ params.setWsfed_home_realm("dummyIdentityProvider");
+
+ doReturn(identityProvider).when(mockHelper.getRealm()).getIdentityProviderByAlias("dummyIdentityProvider");
+ doReturn(new HashSet<>(Arrays.asList(params.getWsfed_reply()))).when(mockHelper.getClient()).getRedirectUris();
+ AuthenticationFlowModel flow = mock(AuthenticationFlowModel.class);
+ doReturn(UUID.randomUUID().toString()).when(flow).getId();
+ doReturn(flow).when(mockHelper.getRealm()).getBrowserFlow();
+
+ doReturn(SslRequired.EXTERNAL).when(mockHelper.getRealm()).getSslRequired();
+ doReturn(new MultivaluedMapImpl()).when(response).getOutputHeaders();
+
+ ResteasyProviderFactory.pushContext(HttpResponse.class, response);
+
+ ClientSessionModel clientSession = mock(ClientSessionModel.class);
+ when(clientSession.getId()).thenReturn(UUID.randomUUID().toString());
+ //when(clientSession.getNote(ClientSessionModel.ACTION_KEY)).thenReturn(KeycloakModelUtils.generateCodeSecret()); //This is normally set in method but because we are mocked we need to return it
+ when(clientSession.getClient()).thenReturn(mockHelper.getClient());
+
+ UserSessionProvider provider = mockHelper.getSession().sessions();
+ doReturn(clientSession).when(provider).createClientSession(mockHelper.getRealm(), mockHelper.getClient());
+
+ Response response = service.handleLoginRequest(params, mockHelper.getClient(), true);
+ assertNotNull(response);
+
+ // The status code is going to change to 303 or 302!
+ assertEquals(response.getStatus(), Response.Status.FOUND.getStatusCode());
+ assertNotNull(response.getLocation());
+
+ assertThat(response.getLocation().getPath(), new EndsWith("broker/dummyIdentityProvider/login"));
+ }
}
\ No newline at end of file
diff --git a/src/test/java/com/quest/keycloak/protocol/wsfed/builders/WsFedSAML11AssertionTypeBuilderTest.java b/src/test/java/com/quest/keycloak/protocol/wsfed/builders/WsFedSAML11AssertionTypeBuilderTest.java
new file mode 100644
index 0000000..eae568f
--- /dev/null
+++ b/src/test/java/com/quest/keycloak/protocol/wsfed/builders/WsFedSAML11AssertionTypeBuilderTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2016 Analytical Graphics, 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 com.quest.keycloak.protocol.wsfed.builders;
+
+import com.quest.keycloak.common.wsfed.MockHelper;
+import com.quest.keycloak.common.wsfed.TestHelpers;
+import com.quest.keycloak.protocol.wsfed.builders.WsFedSAML11AssertionTypeBuilder;
+import com.quest.keycloak.protocol.wsfed.mappers.WSFedSAMLAttributeStatementMapper;
+import com.quest.keycloak.protocol.wsfed.mappers.WSFedSAMLRoleListMapper;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.dom.saml.v1.assertion.SAML11AssertionType;
+import org.keycloak.dom.saml.v1.assertion.SAML11AttributeStatementType;
+import org.keycloak.dom.saml.v1.assertion.SAML11AudienceRestrictionCondition;
+import org.keycloak.dom.saml.v1.assertion.SAML11AuthenticationStatementType;
+import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.saml.common.constants.GeneralConstants;
+import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
+import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
+import org.mockito.MockitoAnnotations;
+
+import java.net.URI;
+import java.util.UUID;
+
+import static com.quest.keycloak.protocol.wsfed.builders.SAML11AssertionTypeBuilder.CLOCK_SKEW;
+import static com.quest.keycloak.protocol.wsfed.builders.WsFedSAMLAssertionTypeAbstractBuilder.WSFED_NAME_ID_FORMAT;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.any;
+
+/**
+ * @author Peter Nalyvayko
+ * @version $Revision: 1 $
+ * @date 10/8/2016
+ */
+
+public class WsFedSAML11AssertionTypeBuilderTest {
+
+ private MockHelper mockHelper;
+
+ @Before
+ public void Before() {
+ MockitoAnnotations.initMocks(this);
+ mockHelper = TestHelpers.getMockHelper();
+ }
+
+ @Test
+ public void testSamlTokenGeneration() throws Exception {
+
+// mockHelper.getClientAttributes().put(WSFedSAML11AssertionTypeBuilder, "false");
+ mockHelper.getClientSessionNotes().put(GeneralConstants.NAMEID_FORMAT, JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get());
+
+ //Role Mapper
+ ProtocolMapperModel roleMappingModel = mock(ProtocolMapperModel.class);
+ when(roleMappingModel.getProtocolMapper()).thenReturn(UUID.randomUUID().toString());
+ WSFedSAMLRoleListMapper roleListMapper = mock(WSFedSAMLRoleListMapper.class);
+ mockHelper.getProtocolMappers().put(roleMappingModel, roleListMapper);
+
+ //Attribute Mapper
+ ProtocolMapperModel attributeMappingModel = mock(ProtocolMapperModel.class);
+ when(attributeMappingModel.getProtocolMapper()).thenReturn(UUID.randomUUID().toString());
+ WSFedSAMLAttributeStatementMapper attributeMapper = mock(WSFedSAMLAttributeStatementMapper.class);
+ mockHelper.getProtocolMappers().put(attributeMappingModel, attributeMapper);
+
+
+ mockHelper.initializeMockValues();
+
+ //SAML Token generation
+ WsFedSAML11AssertionTypeBuilder samlBuilder = new WsFedSAML11AssertionTypeBuilder();
+ samlBuilder.setRealm(mockHelper.getRealm())
+ .setUriInfo(mockHelper.getUriInfo())
+ .setAccessCode(mockHelper.getAccessCode())
+ .setClientSession(mockHelper.getClientSessionModel())
+ .setUserSession(mockHelper.getUserSessionModel())
+ .setSession(mockHelper.getSession());
+
+ SAML11AssertionType token = samlBuilder.build();
+
+ assertNotNull(token);
+
+ assertEquals(String.format("%s/realms/%s", mockHelper.getBaseUri(), mockHelper.getRealmName()), token.getIssuer());
+ // TODO fix me! Check the specs if the name id format is a part of the SAML 1.1 token
+// assertEquals(URI.create(mockHelper.getClientSessionNotes().get(WSFED_NAME_ID_FORMAT)), JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED);
+// TODO: fix me!
+ //assertEquals(mockHelper.getEmail(), token.);
+
+ assertNotNull(token.getIssueInstant());
+ assertNotNull(token.getConditions().getNotBefore());
+ assertNotNull(token.getConditions().getNotOnOrAfter());
+
+ assertNotNull(token.getStatements());
+ assertNotNull(token.getConditions().getNotOnOrAfter());
+
+ // Verify that the token time is within the time interval specified by the conditions statement
+ // and that the time interval is adjusted by a small amount to account for clock skew
+ assertEquals(token.getConditions().getNotBefore(), XMLTimeUtil.subtract(token.getIssueInstant(), CLOCK_SKEW));
+ assertEquals(XMLTimeUtil.add(token.getConditions().getNotBefore(), mockHelper.getAccessTokenLifespanForImplicitFlow() * 1000 + CLOCK_SKEW + CLOCK_SKEW), token.getConditions().getNotOnOrAfter());
+ assertEquals(XMLTimeUtil.add(token.getIssueInstant(), mockHelper.getAccessTokenLifespanForImplicitFlow() * 1000 + CLOCK_SKEW), token.getConditions().getNotOnOrAfter());
+
+ assertEquals(mockHelper.getClientId(), ((SAML11AudienceRestrictionCondition) token.getConditions().get().get(0)).get().get(0).toString());
+
+ assertTrue(token.getStatements().size() > 1);
+ assertNotNull(token.getStatements().get(1));
+ assertTrue(token.getStatements().get(1) instanceof SAML11AuthenticationStatementType);
+
+ SAML11AuthenticationStatementType authType = (SAML11AuthenticationStatementType)token.getStatements().get(1);
+ assertEquals(authType.getAuthenticationInstant(), token.getIssueInstant());
+ assertEquals(authType.getAuthenticationMethod().toString(), JBossSAMLURIConstants.AC_PASSWORD_PROTECTED_TRANSPORT.get());
+ assertEquals(authType.getSubject().getSubjectConfirmation().getConfirmationMethod().get(0), URI.create("urn:oasis:names:tc:SAML:1.0:cm:bearer"));
+
+ ClientSessionModel clientSession = mockHelper.getClientSessionModel();
+ verify(clientSession, times(1)).setNote(WsFedSAML11AssertionTypeBuilder.WSFED_NAME_ID, mockHelper.getUserName());
+ verify(clientSession, times(1)).setNote(WSFED_NAME_ID_FORMAT, mockHelper.getClientSessionNotes().get(GeneralConstants.NAMEID_FORMAT));
+
+ verify(roleListMapper, times(1)).mapRoles(any(AttributeStatementType.class), eq(roleMappingModel), eq(mockHelper.getSession()), eq(mockHelper.getUserSessionModel()), eq(mockHelper.getClientSessionModel()));
+ verify(attributeMapper, times(1)).transformAttributeStatement(any(AttributeStatementType.class), eq(attributeMappingModel), eq(mockHelper.getSession()), eq(mockHelper.getUserSessionModel()), eq(mockHelper.getClientSessionModel()));
+
+ }
+}