diff --git a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/SamlProtocolConstants.java b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/SamlProtocolConstants.java
new file mode 100644
index 000000000000..c6ff173c209e
--- /dev/null
+++ b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/SamlProtocolConstants.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to Apereo under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work
+ * for additional information regarding copyright ownership.
+ * Apereo licenses this file to you 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 the following location:
+ *
+ * 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.jasig.cas.support.saml;
+
+/**
+ * Class that exposes relevant constants and parameters to
+ * the SAML protocol. These include attribute names, pre-defined
+ * values and expected request parameter names as is specified
+ * by the protocol.
+ *
+ * @author Misagh Moayyed
+ * @since 4.1
+ */
+public interface SamlProtocolConstants {
+ /** Constant representing the saml request. */
+ String PARAMETER_SAML_REQUEST = "SAMLRequest";
+
+ /** Constant representing the saml response. */
+ String PARAMETER_SAML_RESPONSE = "SAMLResponse";
+
+ /** Constant representing the saml relay state. */
+ String PARAMETER_SAML_RELAY_STATE = "RelayState";
+
+}
diff --git a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/authentication/principal/GoogleAccountsService.java b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/authentication/principal/GoogleAccountsService.java
index b5ad0c522af0..78c6eebdc7ad 100644
--- a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/authentication/principal/GoogleAccountsService.java
+++ b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/authentication/principal/GoogleAccountsService.java
@@ -18,32 +18,31 @@
*/
package org.jasig.cas.support.saml.authentication.principal;
-import org.apache.commons.codec.binary.Base64;
-import org.apache.commons.io.IOUtils;
+
import org.jasig.cas.authentication.principal.AbstractWebApplicationService;
import org.jasig.cas.authentication.principal.Response;
import org.jasig.cas.services.RegisteredService;
import org.jasig.cas.services.ServicesManager;
-import org.jasig.cas.support.saml.util.SamlUtils;
+import org.jasig.cas.support.saml.util.AbstractSaml20ObjectBuilder;
import org.jasig.cas.util.ISOStandardDateFormat;
import org.jdom.Document;
import org.springframework.util.StringUtils;
-
+import org.jasig.cas.support.saml.SamlProtocolConstants;
+import org.jasig.cas.support.saml.util.GoogleSaml20ObjectBuilder;
+import org.joda.time.DateTime;
+import org.opensaml.saml2.core.Assertion;
+import org.opensaml.saml2.core.AuthnContext;
+import org.opensaml.saml2.core.AuthnStatement;
+import org.opensaml.saml2.core.Conditions;
+import org.opensaml.saml2.core.NameID;
+import org.opensaml.saml2.core.StatusCode;
+import org.opensaml.saml2.core.Subject;
import javax.servlet.http.HttpServletRequest;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.UnsupportedEncodingException;
-import java.nio.charset.Charset;
+import java.io.StringWriter;
import java.security.PrivateKey;
import java.security.PublicKey;
-import java.security.SecureRandom;
-import java.util.Calendar;
-import java.util.Date;
import java.util.HashMap;
import java.util.Map;
-import java.util.zip.DataFormatException;
-import java.util.zip.Inflater;
-import java.util.zip.InflaterInputStream;
import org.jdom.Element;
/**
@@ -63,49 +62,7 @@ public class GoogleAccountsService extends AbstractWebApplicationService {
private static final int HEX_HIGH_BITS_BITWISE_FLAG = 0x0f;
private static final int HEX_RIGHT_SHIFT_COEFFICIENT = 4;
- private static SecureRandom RANDOM_GENERATOR = new SecureRandom();
-
- private static final char[] CHAR_MAPPINGS = {
- 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p'};
-
- private static final String CONST_PARAM_SERVICE = "SAMLRequest";
-
- private static final String CONST_RELAY_STATE = "RelayState";
-
- private static final String TEMPLATE_SAML_RESPONSE =
- "\" IssueInstant=\"\" Version=\"2.0\""
- + " xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\""
- + " xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\""
- + " xmlns:xenc=\"http://www.w3.org/2001/04/xmlenc#\">"
- + ""
- + ""
- + ""
- + "\""
- + " IssueInstant=\"2003-04-17T00:46:02Z\" Version=\"2.0\""
- + " xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">"
- + "https://www.opensaml.org/IDP"
- + ""
- + ""
- + ""
- + ""
- + ""
- + "\" NotOnOrAfter=\"\" InResponseTo=\"\" />"
- + ""
- + ""
- + "\">"
- + ""
- + ""
- + ""
- + ""
- + "\">"
- + ""
- + ""
- + "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
- + ""
- + ""
- + ""
- + "";
+ private static final GoogleSaml20ObjectBuilder BUILDER = new GoogleSaml20ObjectBuilder();
private final String relayState;
@@ -154,6 +111,8 @@ protected GoogleAccountsService(final String id, final String originalUrl,
this.publicKey = publicKey;
this.requestId = requestId;
this.servicesManager = servicesManager;
+
+
}
/**
@@ -168,15 +127,16 @@ protected GoogleAccountsService(final String id, final String originalUrl,
public static GoogleAccountsService createServiceFrom(
final HttpServletRequest request, final PrivateKey privateKey,
final PublicKey publicKey, final ServicesManager servicesManager) {
- final String relayState = request.getParameter(CONST_RELAY_STATE);
+ final String relayState = request.getParameter(SamlProtocolConstants.PARAMETER_SAML_RELAY_STATE);
- final String xmlRequest = decodeAuthnRequestXML(request.getParameter(CONST_PARAM_SERVICE));
+ final String xmlRequest = BUILDER.decodeSamlAuthnRequest(
+ request.getParameter(SamlProtocolConstants.PARAMETER_SAML_REQUEST));
if (!StringUtils.hasText(xmlRequest)) {
return null;
}
- final Document document = SamlUtils.constructDocumentFromXmlString(xmlRequest);
+ final Document document = AbstractSaml20ObjectBuilder.constructDocumentFromXml(xmlRequest);
if (document == null) {
return null;
@@ -194,10 +154,10 @@ public static GoogleAccountsService createServiceFrom(
public Response getResponse(final String ticketId) {
final Map parameters = new HashMap();
final String samlResponse = constructSamlResponse();
- final String signedResponse = SamlUtils.signSamlResponse(samlResponse,
+ final String signedResponse = BUILDER.signSamlResponse(samlResponse,
this.privateKey, this.publicKey);
- parameters.put("SAMLResponse", signedResponse);
- parameters.put("RelayState", this.relayState);
+ parameters.put(SamlProtocolConstants.PARAMETER_SAML_RESPONSE, signedResponse);
+ parameters.put(SamlProtocolConstants.PARAMETER_SAML_RELAY_STATE, this.relayState);
return Response.getPostResponse(getOriginalUrl(), parameters);
}
@@ -214,150 +174,42 @@ public boolean isLoggedOutAlready() {
/**
* Construct SAML response.
- *
+ * See this reference for more info.
* @return the SAML response
*/
private String constructSamlResponse() {
- String samlResponse = TEMPLATE_SAML_RESPONSE;
+ final DateTime currentDateTime = DateTime.parse(new ISOStandardDateFormat().getCurrentDateAndTime());
+ final DateTime notBeforeIssueInstant = DateTime.parse("2003-04-17T00:46:02Z");
- final Calendar c = Calendar.getInstance();
- c.setTime(new Date());
- c.add(Calendar.YEAR, 1);
-
final RegisteredService svc = this.servicesManager.findServiceBy(this);
final String userId = svc.getUsernameAttributeProvider().resolveUsername(getPrincipal(), this);
- final String currentDateTime = new ISOStandardDateFormat().getCurrentDateAndTime();
- samlResponse = samlResponse.replace("", userId);
- samlResponse = samlResponse.replace("", createID());
- samlResponse = samlResponse.replace("", currentDateTime);
- samlResponse = samlResponse.replace("", currentDateTime);
- samlResponse = samlResponse.replaceAll("", currentDateTime);
- samlResponse = samlResponse.replace("", createID());
- samlResponse = samlResponse.replaceAll("", getId());
- return samlResponse.replace("", this.requestId);
- }
+ final org.opensaml.saml2.core.Response response = BUILDER.newResponse(
+ BUILDER.generateSecureRandomId(),
+ currentDateTime,
+ getId(), this);
+ response.setStatus(BUILDER.newStatus(StatusCode.SUCCESS_URI, null));
- /**
- * Creates the SAML id.
- *
- * @return the id
- */
- private static String createID() {
- final byte[] bytes = new byte[SAML_RESPONSE_RANDOM_ID_LENGTH]; // 160 bits
- RANDOM_GENERATOR.nextBytes(bytes);
+ final AuthnStatement authnStatement = BUILDER.newAuthnStatement(
+ AuthnContext.PASSWORD_AUTHN_CTX, currentDateTime);
+ final Assertion assertion = BUILDER.newAssertion(authnStatement,
+ "https://www.opensaml.org/IDP",
+ notBeforeIssueInstant, BUILDER.generateSecureRandomId());
- final char[] chars = new char[SAML_RESPONSE_ID_LENGTH];
+ final Conditions conditions = BUILDER.newConditions(notBeforeIssueInstant,
+ currentDateTime, getId());
+ assertion.setConditions(conditions);
- for (int i = 0; i < bytes.length; i++) {
- final int left = bytes[i] >> HEX_RIGHT_SHIFT_COEFFICIENT & HEX_HIGH_BITS_BITWISE_FLAG;
- final int right = bytes[i] & HEX_HIGH_BITS_BITWISE_FLAG;
- chars[i * 2] = CHAR_MAPPINGS[left];
- chars[i * 2 + 1] = CHAR_MAPPINGS[right];
- }
+ final Subject subject = BUILDER.newSubject(NameID.EMAIL, userId,
+ getId(), currentDateTime, this.requestId);
+ assertion.setSubject(subject);
- return String.valueOf(chars);
- }
+ response.getAssertions().add(assertion);
- /**
- * Decode authn request xml.
- *
- * @param encodedRequestXmlString the encoded request xml string
- * @return the request
- */
- private static String decodeAuthnRequestXML(
- final String encodedRequestXmlString) {
- if (encodedRequestXmlString == null) {
- return null;
- }
-
- final byte[] decodedBytes = base64Decode(encodedRequestXmlString);
-
- if (decodedBytes == null) {
- return null;
- }
-
- final String inflated = inflate(decodedBytes);
-
- if (inflated != null) {
- return inflated;
- }
-
- return zlibDeflate(decodedBytes);
- }
+ final StringWriter writer = new StringWriter();
+ BUILDER.marshalSamlXmlObject(response, writer);
- /**
- * Deflate the given bytes using zlib.
- *
- * @param bytes the bytes
- * @return the converted string
- */
- private static String zlibDeflate(final byte[] bytes) {
- final ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
- final ByteArrayOutputStream baos = new ByteArrayOutputStream();
- final InflaterInputStream iis = new InflaterInputStream(bais);
- final byte[] buf = new byte[DEFLATED_BYTE_ARRAY_BUFFER_LENGTH];
-
- try {
- int count = iis.read(buf);
- while (count != -1) {
- baos.write(buf, 0, count);
- count = iis.read(buf);
- }
- return new String(baos.toByteArray(), Charset.defaultCharset());
- } catch (final Exception e) {
- return null;
- } finally {
- IOUtils.closeQuietly(iis);
- }
- }
-
- /**
- * Base64 decode.
- *
- * @param xml the xml
- * @return the byte[]
- */
- private static byte[] base64Decode(final String xml) {
- try {
- final byte[] xmlBytes = xml.getBytes("UTF-8");
- return Base64.decodeBase64(xmlBytes);
- } catch (final Exception e) {
- return null;
- }
- }
-
- /**
- * Inflate the given byte array.
- *
- * @param bytes the bytes
- * @return the string
- */
- private static String inflate(final byte[] bytes) {
- final Inflater inflater = new Inflater(true);
- final byte[] xmlMessageBytes = new byte[INFLATED_BYTE_ARRAY_LENGTH];
-
- final byte[] extendedBytes = new byte[bytes.length + 1];
- System.arraycopy(bytes, 0, extendedBytes, 0, bytes.length);
- extendedBytes[bytes.length] = 0;
-
- inflater.setInput(extendedBytes);
-
- try {
- final int resultLength = inflater.inflate(xmlMessageBytes);
- inflater.end();
-
- if (!inflater.finished()) {
- throw new RuntimeException("buffer not large enough.");
- }
-
- inflater.end();
- return new String(xmlMessageBytes, 0, resultLength, "UTF-8");
- } catch (final DataFormatException e) {
- return null;
- } catch (final UnsupportedEncodingException e) {
- throw new RuntimeException("Cannot find encoding: UTF-8", e);
- }
+ logger.debug("Generated Google SAML response: {}", writer.toString());
+ return writer.toString();
}
-
}
diff --git a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/AbstractSaml20ObjectBuilder.java b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/AbstractSaml20ObjectBuilder.java
new file mode 100644
index 000000000000..65b66e2b8d59
--- /dev/null
+++ b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/AbstractSaml20ObjectBuilder.java
@@ -0,0 +1,352 @@
+/*
+ * Licensed to Apereo under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work
+ * for additional information regarding copyright ownership.
+ * Apereo licenses this file to you 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 the following location:
+ *
+ * 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.jasig.cas.support.saml.util;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.io.IOUtils;
+import org.jasig.cas.authentication.principal.WebApplicationService;
+import org.jasig.cas.support.saml.authentication.principal.SamlService;
+import org.joda.time.DateTime;
+import org.opensaml.common.SAMLVersion;
+import org.opensaml.saml2.core.Assertion;
+import org.opensaml.saml2.core.Audience;
+import org.opensaml.saml2.core.AudienceRestriction;
+import org.opensaml.saml2.core.AuthnContext;
+import org.opensaml.saml2.core.AuthnContextClassRef;
+import org.opensaml.saml2.core.AuthnStatement;
+import org.opensaml.saml2.core.Conditions;
+import org.opensaml.saml2.core.Issuer;
+import org.opensaml.saml2.core.NameID;
+import org.opensaml.saml2.core.Response;
+import org.opensaml.saml2.core.Status;
+import org.opensaml.saml2.core.StatusCode;
+import org.opensaml.saml2.core.StatusMessage;
+import org.opensaml.saml2.core.Subject;
+import org.opensaml.saml2.core.SubjectConfirmation;
+import org.opensaml.saml2.core.SubjectConfirmationData;
+import org.springframework.util.StringUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.SecureRandom;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+/**
+ * This is {@link AbstractSaml20ObjectBuilder}.
+ * to build saml2 objects.
+ * @author Misagh Moayyed mmoayyed@unicon.net
+ * @since 4.1
+ */
+public abstract class AbstractSaml20ObjectBuilder extends AbstractSamlObjectBuilder {
+ private static final int HEX_HIGH_BITS_BITWISE_FLAG = 0x0f;
+
+ /**
+ * Gets name id.
+ *
+ * @param nameIdFormat the name id format
+ * @param nameIdValue the name id value
+ * @return the name iD
+ */
+ protected NameID getNameID(final String nameIdFormat, final String nameIdValue) {
+ final NameID nameId = newSamlObject(NameID.class);
+ nameId.setFormat(nameIdFormat);
+ nameId.setValue(nameIdValue);
+ return nameId;
+ }
+
+ /**
+ * Create a new SAML response object.
+ * @param id the id
+ * @param issueInstant the issue instant
+ * @param recipient the recipient
+ * @param service the service
+ * @return the response
+ */
+ public Response newResponse(final String id, final DateTime issueInstant,
+ final String recipient, final WebApplicationService service) {
+
+ final Response samlResponse = newSamlObject(Response.class);
+ samlResponse.setID(id);
+ samlResponse.setIssueInstant(issueInstant);
+ samlResponse.setVersion(SAMLVersion.VERSION_20);
+ if (service instanceof SamlService) {
+ final SamlService samlService = (SamlService) service;
+
+ if (samlService.getRequestID() != null) {
+ samlResponse.setInResponseTo(samlService.getRequestID());
+ }
+ }
+ return samlResponse;
+ }
+
+ /**
+ * Create a new SAML status object.
+ *
+ * @param codeValue the code value
+ * @param statusMessage the status message
+ * @return the status
+ */
+ public Status newStatus(final String codeValue, final String statusMessage) {
+ final Status status = newSamlObject(Status.class);
+ final StatusCode code = newSamlObject(StatusCode.class);
+ code.setValue(codeValue);
+ status.setStatusCode(code);
+ if (StringUtils.hasText(statusMessage)) {
+ final StatusMessage message = newSamlObject(StatusMessage.class);
+ message.setMessage(statusMessage);
+ status.setStatusMessage(message);
+ }
+ return status;
+ }
+
+ /**
+ * Create a new SAML1 response object.
+ *
+ * @param authnStatement the authn statement
+ * @param issuer the issuer
+ * @param issuedAt the issued at
+ * @param id the id
+ * @return the assertion
+ */
+ public Assertion newAssertion(final AuthnStatement authnStatement, final String issuer,
+ final DateTime issuedAt, final String id) {
+ final Assertion assertion = newSamlObject(Assertion.class);
+ assertion.setID(id);
+ assertion.setIssueInstant(issuedAt);
+ assertion.setIssuer(newIssuer(issuer));
+ assertion.getAuthnStatements().add(authnStatement);
+ return assertion;
+ }
+
+ /**
+ * New issuer.
+ *
+ * @param issuerValue the issuer
+ * @return the issuer
+ */
+ public Issuer newIssuer(final String issuerValue) {
+ final Issuer issuer = newSamlObject(Issuer.class);
+ issuer.setValue(issuerValue);
+ return issuer;
+ }
+
+ /**
+ * New authn statement.
+ *
+ * @param contextClassRef the context class ref such as {@link AuthnContext#PASSWORD_AUTHN_CTX}
+ * @param authnInstant the authn instant
+ * @return the authn statement
+ */
+ public AuthnStatement newAuthnStatement(final String contextClassRef, final DateTime authnInstant) {
+ final AuthnStatement stmt = newSamlObject(AuthnStatement.class);
+ final AuthnContext ctx = newSamlObject(AuthnContext.class);
+
+ final AuthnContextClassRef classRef = newSamlObject(AuthnContextClassRef.class);
+ classRef.setAuthnContextClassRef(contextClassRef);
+
+ ctx.setAuthnContextClassRef(classRef);
+ stmt.setAuthnContext(ctx);
+ stmt.setAuthnInstant(authnInstant);
+
+ return stmt;
+ }
+
+ /**
+ * New conditions element.
+ *
+ * @param notBefore the not before
+ * @param notOnOrAfter the not on or after
+ * @param audienceUri the service id
+ * @return the conditions
+ */
+ public Conditions newConditions(final DateTime notBefore, final DateTime notOnOrAfter, final String audienceUri) {
+ final Conditions conditions = newSamlObject(Conditions.class);
+ conditions.setNotBefore(notBefore);
+ conditions.setNotOnOrAfter(notOnOrAfter);
+
+ final AudienceRestriction audienceRestriction = newSamlObject(AudienceRestriction.class);
+ final Audience audience = newSamlObject(Audience.class);
+ audience.setAudienceURI(audienceUri);
+ audienceRestriction.getAudiences().add(audience);
+ conditions.getAudienceRestrictions().add(audienceRestriction);
+ return conditions;
+ }
+
+ /**
+ * New subject element.
+ *
+ * @param nameIdFormat the name id format
+ * @param nameIdValue the name id value
+ * @param recipient the recipient
+ * @param notOnOrAfter the not on or after
+ * @param inResponseTo the in response to
+ * @return the subject
+ */
+ public Subject newSubject(final String nameIdFormat, final String nameIdValue,
+ final String recipient, final DateTime notOnOrAfter,
+ final String inResponseTo) {
+
+ final SubjectConfirmation confirmation = newSamlObject(SubjectConfirmation.class);
+ confirmation.setMethod(SubjectConfirmation.METHOD_BEARER);
+
+ final SubjectConfirmationData data = newSamlObject(SubjectConfirmationData.class);
+ data.setRecipient(recipient);
+ data.setNotOnOrAfter(notOnOrAfter);
+ data.setInResponseTo(inResponseTo);
+
+ confirmation.setSubjectConfirmationData(data);
+
+ final Subject subject = newSamlObject(Subject.class);
+ subject.setNameID(getNameID(nameIdFormat, nameIdValue));
+ subject.getSubjectConfirmations().add(confirmation);
+ return subject;
+ }
+
+ @Override
+ public String generateSecureRandomId() {
+ final SecureRandom generator = new SecureRandom();
+ final char[] charMappings = {
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
+ 'p'};
+
+ final int charsLength = 40;
+ final int generatorBytesLength = 20;
+ final int shiftLength = 4;
+
+ // 160 bits
+ final byte[] bytes = new byte[generatorBytesLength];
+ generator.nextBytes(bytes);
+
+ final char[] chars = new char[charsLength];
+ for (int i = 0; i < bytes.length; i++) {
+ final int left = bytes[i] >> shiftLength & HEX_HIGH_BITS_BITWISE_FLAG;
+ final int right = bytes[i] & HEX_HIGH_BITS_BITWISE_FLAG;
+ chars[i * 2] = charMappings[left];
+ chars[i * 2 + 1] = charMappings[right];
+ }
+ return String.valueOf(chars);
+ }
+
+ /**
+ * Deflate the given bytes using zlib.
+ *
+ * @param bytes the bytes
+ * @return the converted string
+ */
+ private static String zlibDeflate(final byte[] bytes) {
+ final int bufferLength = 1024;
+ final ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ final InflaterInputStream iis = new InflaterInputStream(bais);
+ final byte[] buf = new byte[bufferLength];
+
+ try {
+ int count = iis.read(buf);
+ while (count != -1) {
+ baos.write(buf, 0, count);
+ count = iis.read(buf);
+ }
+ return new String(baos.toByteArray());
+ } catch (final Exception e) {
+ return null;
+ } finally {
+ IOUtils.closeQuietly(iis);
+ }
+ }
+
+ /**
+ * Base64 decode.
+ *
+ * @param xml the xml
+ * @return the byte[]
+ */
+ private static byte[] base64Decode(final String xml) {
+ try {
+ final byte[] xmlBytes = xml.getBytes("UTF-8");
+ return Base64.decodeBase64(xmlBytes);
+ } catch (final Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * Inflate the given byte array.
+ *
+ * @param bytes the bytes
+ * @return the string
+ */
+ private static String inflate(final byte[] bytes) {
+ final int bufferLength = 10000;
+ final Inflater inflater = new Inflater(true);
+ final byte[] xmlMessageBytes = new byte[bufferLength];
+
+ final byte[] extendedBytes = new byte[bytes.length + 1];
+ System.arraycopy(bytes, 0, extendedBytes, 0, bytes.length);
+ extendedBytes[bytes.length] = 0;
+
+ inflater.setInput(extendedBytes);
+
+ try {
+ final int resultLength = inflater.inflate(xmlMessageBytes);
+ inflater.end();
+
+ if (!inflater.finished()) {
+ throw new RuntimeException("buffer not large enough.");
+ }
+
+ inflater.end();
+ return new String(xmlMessageBytes, 0, resultLength, "UTF-8");
+ } catch (final DataFormatException e) {
+ return null;
+ } catch (final UnsupportedEncodingException e) {
+ throw new RuntimeException("Cannot find encoding: UTF-8", e);
+ }
+ }
+
+
+
+ /**
+ * Decode authn request xml.
+ *
+ * @param encodedRequestXmlString the encoded request xml string
+ * @return the request
+ */
+ public String decodeSamlAuthnRequest(final String encodedRequestXmlString) {
+ if (encodedRequestXmlString == null) {
+ return null;
+ }
+
+ final byte[] decodedBytes = base64Decode(encodedRequestXmlString);
+ if (decodedBytes == null) {
+ return null;
+ }
+
+ final String inflated = inflate(decodedBytes);
+ if (inflated != null) {
+ return inflated;
+ }
+
+ return zlibDeflate(decodedBytes);
+ }
+}
diff --git a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/SamlUtils.java b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/AbstractSamlObjectBuilder.java
similarity index 51%
rename from cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/SamlUtils.java
rename to cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/AbstractSamlObjectBuilder.java
index 3c4910936dd9..af1740854af3 100644
--- a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/SamlUtils.java
+++ b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/AbstractSamlObjectBuilder.java
@@ -1,254 +1,396 @@
-/*
- * Licensed to Apereo under one or more contributor license
- * agreements. See the NOTICE file distributed with this work
- * for additional information regarding copyright ownership.
- * Apereo licenses this file to you 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 the following location:
- *
- * 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.jasig.cas.support.saml.util;
-
-import org.jdom.Document;
-import org.jdom.Element;
-import org.jdom.input.DOMBuilder;
-import org.jdom.input.SAXBuilder;
-import org.jdom.output.XMLOutputter;
-import org.w3c.dom.Node;
-
-import javax.xml.crypto.dsig.CanonicalizationMethod;
-import javax.xml.crypto.dsig.DigestMethod;
-import javax.xml.crypto.dsig.Reference;
-import javax.xml.crypto.dsig.SignatureMethod;
-import javax.xml.crypto.dsig.SignedInfo;
-import javax.xml.crypto.dsig.Transform;
-import javax.xml.crypto.dsig.XMLSignature;
-import javax.xml.crypto.dsig.XMLSignatureFactory;
-import javax.xml.crypto.dsig.dom.DOMSignContext;
-import javax.xml.crypto.dsig.keyinfo.KeyInfo;
-import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
-import javax.xml.crypto.dsig.keyinfo.KeyValue;
-import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
-import javax.xml.crypto.dsig.spec.TransformParameterSpec;
-import javax.xml.parsers.DocumentBuilderFactory;
-import java.io.ByteArrayInputStream;
-import java.io.StringWriter;
-import java.nio.charset.Charset;
-import java.security.PrivateKey;
-import java.security.Provider;
-import java.security.PublicKey;
-import java.security.interfaces.DSAPublicKey;
-import java.security.interfaces.RSAPublicKey;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Utilities adopted from the Google sample code.
- *
- * @author Scott Battaglia
- * @since 3.1
- */
-public final class SamlUtils {
-
- private static final String JSR_105_PROVIDER = "org.jcp.xml.dsig.internal.dom.XMLDSigRI";
-
- private static final String SAML_PROTOCOL_NS_URI_V20 = "urn:oasis:names:tc:SAML:2.0:protocol";
-
- /**
- * The constructor is intentionally marked as private.
- */
- private SamlUtils() {
- // nothing to do
- }
-
- /**
- * Sign SAML response.
- *
- * @param samlResponse the SAML response
- * @param privateKey the private key
- * @param publicKey the public key
- * @return the response
- */
- public static String signSamlResponse(final String samlResponse,
- final PrivateKey privateKey, final PublicKey publicKey) {
- final Document doc = constructDocumentFromXmlString(samlResponse);
-
- if (doc != null) {
- final Element signedElement = signSamlElement(doc.getRootElement(),
- privateKey, publicKey);
- doc.setRootElement((Element) signedElement.detach());
- return new XMLOutputter().outputString(doc);
- }
- throw new RuntimeException("Error signing SAML Response: Null document");
- }
-
- /**
- * Construct document from xml string.
- *
- * @param xmlString the xml string
- * @return the document
- */
- public static Document constructDocumentFromXmlString(final String xmlString) {
- try {
- final SAXBuilder builder = new SAXBuilder();
- builder.setFeature("http://xml.org/sax/features/external-general-entities", false);
- builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
- return builder
- .build(new ByteArrayInputStream(xmlString.getBytes(Charset.defaultCharset())));
- } catch (final Exception e) {
- return null;
- }
- }
-
- /**
- * Sign SAML element.
- *
- * @param element the element
- * @param privKey the priv key
- * @param pubKey the pub key
- * @return the element
- */
- private static Element signSamlElement(final Element element, final PrivateKey privKey,
- final PublicKey pubKey) {
- try {
- final String providerName = System.getProperty("jsr105Provider",
- JSR_105_PROVIDER);
- final XMLSignatureFactory sigFactory = XMLSignatureFactory
- .getInstance("DOM", (Provider) Class.forName(providerName)
- .newInstance());
-
- final List envelopedTransform = Collections
- .singletonList(sigFactory.newTransform(Transform.ENVELOPED,
- (TransformParameterSpec) null));
-
- final Reference ref = sigFactory.newReference("", sigFactory
- .newDigestMethod(DigestMethod.SHA1, null), envelopedTransform,
- null, null);
-
- // Create the SignatureMethod based on the type of key
- SignatureMethod signatureMethod;
- if (pubKey instanceof DSAPublicKey) {
- signatureMethod = sigFactory.newSignatureMethod(
- SignatureMethod.DSA_SHA1, null);
- } else if (pubKey instanceof RSAPublicKey) {
- signatureMethod = sigFactory.newSignatureMethod(
- SignatureMethod.RSA_SHA1, null);
- } else {
- throw new RuntimeException(
- "Error signing SAML element: Unsupported type of key");
- }
-
- final CanonicalizationMethod canonicalizationMethod = sigFactory
- .newCanonicalizationMethod(
- CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS,
- (C14NMethodParameterSpec) null);
-
- // Create the SignedInfo
- final SignedInfo signedInfo = sigFactory.newSignedInfo(
- canonicalizationMethod, signatureMethod, Collections
- .singletonList(ref));
-
- // Create a KeyValue containing the DSA or RSA PublicKey
- final KeyInfoFactory keyInfoFactory = sigFactory
- .getKeyInfoFactory();
- final KeyValue keyValuePair = keyInfoFactory.newKeyValue(pubKey);
-
- // Create a KeyInfo and add the KeyValue to it
- final KeyInfo keyInfo = keyInfoFactory.newKeyInfo(Collections
- .singletonList(keyValuePair));
- // Convert the JDOM document to w3c (Java XML signature API requires
- // w3c
- // representation)
- final org.w3c.dom.Element w3cElement = toDom(element);
-
- // Create a DOMSignContext and specify the DSA/RSA PrivateKey and
- // location of the resulting XMLSignature's parent element
- final DOMSignContext dsc = new DOMSignContext(privKey, w3cElement);
-
- final org.w3c.dom.Node xmlSigInsertionPoint = getXmlSignatureInsertLocation(w3cElement);
- dsc.setNextSibling(xmlSigInsertionPoint);
-
- // Marshal, generate (and sign) the enveloped signature
- final XMLSignature signature = sigFactory.newXMLSignature(signedInfo,
- keyInfo);
- signature.sign(dsc);
-
- return toJdom(w3cElement);
-
- } catch (final Exception e) {
- throw new RuntimeException("Error signing SAML element: "
- + e.getMessage(), e);
- }
- }
-
- /**
- * Gets the xml signature insert location.
- *
- * @param elem the elem
- * @return the xml signature insert location
- */
- private static Node getXmlSignatureInsertLocation(final org.w3c.dom.Element elem) {
- org.w3c.dom.Node insertLocation = null;
- org.w3c.dom.NodeList nodeList = elem.getElementsByTagNameNS(
- SAML_PROTOCOL_NS_URI_V20, "Extensions");
- if (nodeList.getLength() != 0) {
- insertLocation = nodeList.item(nodeList.getLength() - 1);
- } else {
- nodeList = elem.getElementsByTagNameNS(SAML_PROTOCOL_NS_URI_V20,
- "Status");
- insertLocation = nodeList.item(nodeList.getLength() - 1);
- }
- return insertLocation;
- }
-
- /**
- * Convert the received jdom element to an Element.
- *
- * @param element the element
- * @return the org.w3c.dom. element
- */
- private static org.w3c.dom.Element toDom(final Element element) {
- return toDom(element.getDocument()).getDocumentElement();
- }
-
- /**
- * Convert the received jdom doc to a Document element.
- *
- * @param doc the doc
- * @return the org.w3c.dom. document
- */
- private static org.w3c.dom.Document toDom(final Document doc) {
- try {
- final XMLOutputter xmlOutputter = new XMLOutputter();
- final StringWriter elemStrWriter = new StringWriter();
- xmlOutputter.output(doc, elemStrWriter);
- final byte[] xmlBytes = elemStrWriter.toString().getBytes(Charset.defaultCharset());
- final DocumentBuilderFactory dbf = DocumentBuilderFactory
- .newInstance();
- dbf.setNamespaceAware(true);
- return dbf.newDocumentBuilder().parse(
- new ByteArrayInputStream(xmlBytes));
- } catch (final Exception e) {
- return null;
- }
- }
-
- /**
- * Convert to a jdom element.
- *
- * @param e the e
- * @return the element
- */
- private static Element toJdom(final org.w3c.dom.Element e) {
- return new DOMBuilder().build(e);
- }
-}
+/*
+ * Licensed to Apereo under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work
+ * for additional information regarding copyright ownership.
+ * Apereo licenses this file to you 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 the following location:
+ *
+ * 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.jasig.cas.support.saml.util;
+
+import org.jdom.Document;
+import org.jdom.input.DOMBuilder;
+import org.jdom.input.SAXBuilder;
+import org.jdom.output.XMLOutputter;
+import org.opensaml.Configuration;
+import org.opensaml.DefaultBootstrap;
+import org.opensaml.common.SAMLObject;
+import org.opensaml.common.SAMLObjectBuilder;
+import org.opensaml.common.impl.SecureRandomIdentifierGenerator;
+import org.opensaml.common.xml.SAMLConstants;
+import org.opensaml.xml.ConfigurationException;
+import org.opensaml.xml.XMLObject;
+import org.opensaml.xml.io.Marshaller;
+import org.opensaml.xml.io.MarshallerFactory;
+import org.opensaml.xml.schema.XSString;
+import org.opensaml.xml.schema.impl.XSStringBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import javax.xml.crypto.dsig.CanonicalizationMethod;
+import javax.xml.crypto.dsig.DigestMethod;
+import javax.xml.crypto.dsig.Reference;
+import javax.xml.crypto.dsig.SignatureMethod;
+import javax.xml.crypto.dsig.SignedInfo;
+import javax.xml.crypto.dsig.Transform;
+import javax.xml.crypto.dsig.XMLSignature;
+import javax.xml.crypto.dsig.XMLSignatureFactory;
+import javax.xml.crypto.dsig.dom.DOMSignContext;
+import javax.xml.crypto.dsig.keyinfo.KeyInfo;
+import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
+import javax.xml.crypto.dsig.keyinfo.KeyValue;
+import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
+import javax.xml.crypto.dsig.spec.TransformParameterSpec;
+import javax.xml.namespace.QName;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import java.io.ByteArrayInputStream;
+import java.io.StringWriter;
+import java.lang.reflect.Field;
+import java.nio.charset.Charset;
+import java.security.PrivateKey;
+import java.security.Provider;
+import java.security.PublicKey;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.Collections;
+import java.util.List;
+/**
+ * An abstract builder to serve as the template handler
+ * for SAML1 and SAML2 responses.
+ *
+ * @author Misagh Moayyed mmoayyed@unicon.net
+ * @since 4.1
+ */
+public abstract class AbstractSamlObjectBuilder {
+ /**
+ * The constant DEFAULT_ELEMENT_NAME_FIELD.
+ */
+ protected static final String DEFAULT_ELEMENT_NAME_FIELD = "DEFAULT_ELEMENT_NAME";
+
+ /**
+ * The constant DEFAULT_ELEMENT_LOCAL_NAME_FIELD.
+ */
+ protected static final String DEFAULT_ELEMENT_LOCAL_NAME_FIELD = "DEFAULT_ELEMENT_LOCAL_NAME";
+
+ /** Logger instance. **/
+ protected final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ static {
+ try {
+ // Initialize OpenSAML default configuration
+ // (only needed once per classloader)
+ DefaultBootstrap.bootstrap();
+ } catch (final ConfigurationException e) {
+ throw new IllegalStateException("Error initializing OpenSAML library.", e);
+ }
+ }
+
+ /**
+ * Create a new SAML object.
+ *
+ * @param the generic type
+ * @param objectType the object type
+ * @return the t
+ */
+ public final T newSamlObject(final Class objectType) {
+ final QName qName = getSamlObjectQName(objectType);
+ final SAMLObjectBuilder builder = (SAMLObjectBuilder) Configuration.getBuilderFactory().getBuilder(qName);
+ if (builder == null) {
+ throw new IllegalStateException("No SAMLObjectBuilder registered for class " + objectType.getName());
+ }
+ return objectType.cast(builder.buildObject(qName));
+ }
+
+ /**
+ * Gets saml object QName.
+ *
+ * @param objectType the object type
+ * @return the saml object QName
+ * @throws RuntimeException the exception
+ */
+ public QName getSamlObjectQName(final Class objectType) throws RuntimeException {
+ try {
+ final Field f = objectType.getField(DEFAULT_ELEMENT_NAME_FIELD);
+ final QName qName = (QName) f.get(null);
+ return qName;
+ } catch (final NoSuchFieldException e) {
+ throw new IllegalStateException("Cannot find field " + objectType.getName() + "." + DEFAULT_ELEMENT_NAME_FIELD);
+ } catch (final IllegalAccessException e) {
+ throw new IllegalStateException("Cannot access field " + objectType.getName() + "." + DEFAULT_ELEMENT_NAME_FIELD);
+ }
+ }
+
+ /**
+ * Build the saml object based on its QName.
+ *
+ * @param objectType the object
+ * @param qName the QName
+ * @param the object type
+ * @return the saml object
+ */
+ private T newSamlObject(final Class objectType, final QName qName) {
+ final SAMLObjectBuilder builder = (SAMLObjectBuilder) Configuration.getBuilderFactory().getBuilder(qName);
+ if (builder == null) {
+ throw new IllegalStateException("No SAMLObjectBuilder registered for class " + objectType.getName());
+ }
+ return objectType.cast(builder.buildObject());
+ }
+
+ /**
+ * New attribute value.
+ *
+ * @param value the value
+ * @param elementName the element name
+ * @return the xS string
+ */
+ protected final XSString newAttributeValue(final Object value, final QName elementName) {
+ final XSStringBuilder attrValueBuilder = new XSStringBuilder();
+ final XSString stringValue = attrValueBuilder.buildObject(elementName, XSString.TYPE_NAME);
+ if (value instanceof String) {
+ stringValue.setValue((String) value);
+ } else {
+ stringValue.setValue(value.toString());
+ }
+ return stringValue;
+ }
+
+ /**
+ * Generate a secure random id.
+ *
+ * @return the secure id string
+ */
+ public String generateSecureRandomId() {
+ try {
+ final SecureRandomIdentifierGenerator idGenerator = new SecureRandomIdentifierGenerator();
+ return idGenerator.generateIdentifier();
+ } catch (final Exception e) {
+ throw new IllegalStateException("Cannot create secure random ID generator for SAML message IDs.", e);
+ }
+ }
+
+ /**
+ * Marshal the saml xml object to raw xml.
+ *
+ * @param object the object
+ * @param writer the writer
+ * @return the xml string
+ */
+ public String marshalSamlXmlObject(final XMLObject object, final StringWriter writer) {
+ try {
+ final MarshallerFactory marshallerFactory = Configuration.getMarshallerFactory();
+ final Marshaller marshaller = marshallerFactory.getMarshaller(object);
+ final Element element = marshaller.marshall(object);
+ element.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns", SAMLConstants.SAML20_NS);
+ element.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xenc", "http://www.w3.org/2001/04/xmlenc#");
+
+ final TransformerFactory transFactory = TransformerFactory.newInstance();
+ final Transformer transformer = transFactory.newTransformer();
+ transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.transform(new DOMSource(element), new StreamResult(writer));
+ return writer.toString();
+ } catch (final Exception e) {
+ throw new IllegalStateException("An error has occurred while marshalling SAML object to xml", e);
+ }
+ }
+
+ /**
+ * Sign SAML response.
+ *
+ * @param samlResponse the SAML response
+ * @param privateKey the private key
+ * @param publicKey the public key
+ * @return the response
+ */
+ public final String signSamlResponse(final String samlResponse,
+ final PrivateKey privateKey, final PublicKey publicKey) {
+ final Document doc = constructDocumentFromXml(samlResponse);
+
+ if (doc != null) {
+ final org.jdom.Element signedElement = signSamlElement(doc.getRootElement(),
+ privateKey, publicKey);
+ doc.setRootElement((org.jdom.Element) signedElement.detach());
+ return new XMLOutputter().outputString(doc);
+ }
+ throw new RuntimeException("Error signing SAML Response: Null document");
+ }
+
+ /**
+ * Construct document from xml string.
+ *
+ * @param xmlString the xml string
+ * @return the document
+ */
+ public static Document constructDocumentFromXml(final String xmlString) {
+ try {
+ final SAXBuilder builder = new SAXBuilder();
+ builder.setFeature("http://xml.org/sax/features/external-general-entities", false);
+ builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+ return builder
+ .build(new ByteArrayInputStream(xmlString.getBytes(Charset.defaultCharset())));
+ } catch (final Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * Sign SAML element.
+ *
+ * @param element the element
+ * @param privKey the priv key
+ * @param pubKey the pub key
+ * @return the element
+ */
+ private static org.jdom.Element signSamlElement(final org.jdom.Element element, final PrivateKey privKey,
+ final PublicKey pubKey) {
+ try {
+ final String providerName = System.getProperty("jsr105Provider",
+ "org.jcp.xml.dsig.internal.dom.XMLDSigRI");
+
+ final XMLSignatureFactory sigFactory = XMLSignatureFactory
+ .getInstance("DOM", (Provider) Class.forName(providerName)
+ .newInstance());
+
+ final List envelopedTransform = Collections
+ .singletonList(sigFactory.newTransform(Transform.ENVELOPED,
+ (TransformParameterSpec) null));
+
+ final Reference ref = sigFactory.newReference("", sigFactory
+ .newDigestMethod(DigestMethod.SHA1, null), envelopedTransform,
+ null, null);
+
+ // Create the SignatureMethod based on the type of key
+ SignatureMethod signatureMethod;
+ if (pubKey instanceof DSAPublicKey) {
+ signatureMethod = sigFactory.newSignatureMethod(
+ SignatureMethod.DSA_SHA1, null);
+ } else if (pubKey instanceof RSAPublicKey) {
+ signatureMethod = sigFactory.newSignatureMethod(
+ SignatureMethod.RSA_SHA1, null);
+ } else {
+ throw new RuntimeException("Error signing SAML element: Unsupported type of key");
+ }
+
+ final CanonicalizationMethod canonicalizationMethod = sigFactory
+ .newCanonicalizationMethod(
+ CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS,
+ (C14NMethodParameterSpec) null);
+
+ // Create the SignedInfo
+ final SignedInfo signedInfo = sigFactory.newSignedInfo(
+ canonicalizationMethod, signatureMethod, Collections
+ .singletonList(ref));
+
+ // Create a KeyValue containing the DSA or RSA PublicKey
+ final KeyInfoFactory keyInfoFactory = sigFactory
+ .getKeyInfoFactory();
+ final KeyValue keyValuePair = keyInfoFactory.newKeyValue(pubKey);
+
+ // Create a KeyInfo and add the KeyValue to it
+ final KeyInfo keyInfo = keyInfoFactory.newKeyInfo(Collections
+ .singletonList(keyValuePair));
+ // Convert the JDOM document to w3c (Java XML signature API requires
+ // w3c
+ // representation)
+ final org.w3c.dom.Element w3cElement = toDom(element);
+
+ // Create a DOMSignContext and specify the DSA/RSA PrivateKey and
+ // location of the resulting XMLSignature's parent element
+ final DOMSignContext dsc = new DOMSignContext(privKey, w3cElement);
+
+ final org.w3c.dom.Node xmlSigInsertionPoint = getXmlSignatureInsertLocation(w3cElement);
+ dsc.setNextSibling(xmlSigInsertionPoint);
+
+ // Marshal, generate (and sign) the enveloped signature
+ final XMLSignature signature = sigFactory.newXMLSignature(signedInfo,
+ keyInfo);
+ signature.sign(dsc);
+
+ return toJdom(w3cElement);
+
+ } catch (final Exception e) {
+ throw new RuntimeException("Error signing SAML element: "
+ + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Gets the xml signature insert location.
+ *
+ * @param elem the elem
+ * @return the xml signature insert location
+ */
+ private static Node getXmlSignatureInsertLocation(final org.w3c.dom.Element elem) {
+ org.w3c.dom.Node insertLocation = null;
+ org.w3c.dom.NodeList nodeList = elem.getElementsByTagNameNS(
+ SAMLConstants.SAML20P_NS, "Extensions");
+ if (nodeList.getLength() != 0) {
+ insertLocation = nodeList.item(nodeList.getLength() - 1);
+ } else {
+ nodeList = elem.getElementsByTagNameNS(SAMLConstants.SAML20P_NS, "Status");
+ insertLocation = nodeList.item(nodeList.getLength() - 1);
+ }
+ return insertLocation;
+ }
+
+ /**
+ * Convert the received jdom element to an Element.
+ *
+ * @param element the element
+ * @return the org.w3c.dom. element
+ */
+ private static org.w3c.dom.Element toDom(final org.jdom.Element element) {
+ return toDom(element.getDocument()).getDocumentElement();
+ }
+
+ /**
+ * Convert the received jdom doc to a Document element.
+ *
+ * @param doc the doc
+ * @return the org.w3c.dom. document
+ */
+ private static org.w3c.dom.Document toDom(final Document doc) {
+ try {
+ final XMLOutputter xmlOutputter = new XMLOutputter();
+ final StringWriter elemStrWriter = new StringWriter();
+ xmlOutputter.output(doc, elemStrWriter);
+ final byte[] xmlBytes = elemStrWriter.toString().getBytes(Charset.defaultCharset());
+ final DocumentBuilderFactory dbf = DocumentBuilderFactory
+ .newInstance();
+ dbf.setNamespaceAware(true);
+ return dbf.newDocumentBuilder().parse(
+ new ByteArrayInputStream(xmlBytes));
+ } catch (final Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * Convert to a jdom element.
+ *
+ * @param e the e
+ * @return the element
+ */
+ private static org.jdom.Element toJdom(final org.w3c.dom.Element e) {
+ return new DOMBuilder().build(e);
+ }
+}
+
diff --git a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/GoogleSaml20ObjectBuilder.java b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/GoogleSaml20ObjectBuilder.java
new file mode 100644
index 000000000000..4edcf650cee5
--- /dev/null
+++ b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/GoogleSaml20ObjectBuilder.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to Apereo under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work
+ * for additional information regarding copyright ownership.
+ * Apereo licenses this file to you 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 the following location:
+ *
+ * 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.jasig.cas.support.saml.util;
+
+import org.opensaml.common.xml.SAMLConstants;
+import org.opensaml.saml2.core.Response;
+import org.opensaml.saml2.core.Status;
+import org.opensaml.saml2.core.StatusCode;
+
+import javax.xml.XMLConstants;
+import javax.xml.namespace.QName;
+import java.lang.reflect.Field;
+
+/**
+ * This is {@link org.jasig.cas.support.saml.util.GoogleSaml20ObjectBuilder} that
+ * attempts to build the saml response. QName based on the spec described here:
+ * https://developers.google.com/google-apps/sso/saml_reference_implementation_web#samlReferenceImplementationWebSetupChangeDomain
+ * @author Misagh Moayyed mmoayyed@unicon.net
+ * @since 4.1.0
+ */
+public class GoogleSaml20ObjectBuilder extends AbstractSaml20ObjectBuilder {
+ @Override
+ public final QName getSamlObjectQName(final Class objectType) throws RuntimeException {
+ try {
+ final Field f = objectType.getField(DEFAULT_ELEMENT_LOCAL_NAME_FIELD);
+ final String name = f.get(null).toString();
+
+ if (objectType.equals(Response.class) || objectType.equals(Status.class)
+ || objectType.equals(StatusCode.class)) {
+ return new QName(SAMLConstants.SAML20P_NS, name, "samlp");
+ }
+ return new QName(SAMLConstants.SAML20_NS, name, XMLConstants.DEFAULT_NS_PREFIX);
+ } catch (final Exception e){
+ throw new IllegalStateException("Cannot access field " + objectType.getName() + "." + DEFAULT_ELEMENT_LOCAL_NAME_FIELD);
+ }
+ }
+}
diff --git a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/Saml10ObjectBuilder.java b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/Saml10ObjectBuilder.java
new file mode 100644
index 000000000000..3d7a796a9ea6
--- /dev/null
+++ b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/util/Saml10ObjectBuilder.java
@@ -0,0 +1,262 @@
+/*
+ * Licensed to Apereo under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work
+ * for additional information regarding copyright ownership.
+ * Apereo licenses this file to you 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 the following location:
+ *
+ * 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.jasig.cas.support.saml.util;
+
+import org.jasig.cas.authentication.principal.WebApplicationService;
+import org.jasig.cas.support.saml.authentication.SamlAuthenticationMetaDataPopulator;
+import org.jasig.cas.support.saml.authentication.principal.SamlService;
+import org.joda.time.DateTime;
+import org.opensaml.common.SAMLVersion;
+import org.opensaml.common.binding.BasicSAMLMessageContext;
+import org.opensaml.saml1.binding.encoding.HTTPSOAP11Encoder;
+import org.opensaml.saml1.core.Assertion;
+import org.opensaml.saml1.core.Attribute;
+import org.opensaml.saml1.core.AttributeStatement;
+import org.opensaml.saml1.core.AttributeValue;
+import org.opensaml.saml1.core.Audience;
+import org.opensaml.saml1.core.AudienceRestrictionCondition;
+import org.opensaml.saml1.core.AuthenticationStatement;
+import org.opensaml.saml1.core.Conditions;
+import org.opensaml.saml1.core.ConfirmationMethod;
+import org.opensaml.saml1.core.NameIdentifier;
+import org.opensaml.saml1.core.Response;
+import org.opensaml.saml1.core.Status;
+import org.opensaml.saml1.core.StatusCode;
+import org.opensaml.saml1.core.StatusMessage;
+import org.opensaml.saml1.core.Subject;
+import org.opensaml.saml1.core.SubjectConfirmation;
+import org.opensaml.ws.transport.http.HttpServletResponseAdapter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.namespace.QName;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * This is the response builder for Saml1 Protocol.
+ *
+ * @author Misagh Moayyed mmoayyed@unicon.net
+ * @since 4.1
+ */
+public final class Saml10ObjectBuilder extends AbstractSamlObjectBuilder {
+
+ private static final String CONFIRMATION_METHOD = "urn:oasis:names:tc:SAML:1.0:cm:artifact";
+
+ /**
+ * Encoder to wrap the saml response in a SOAP envelope.
+ */
+ private final HTTPSOAP11Encoder encoder = new CasHTTPSOAP11Encoder();
+
+ /**
+ * Create a new SAML response object.
+ * @param id the id
+ * @param issueInstant the issue instant
+ * @param recipient the recipient
+ * @param service the service
+ * @return the response
+ */
+ public Response newResponse(final String id, final DateTime issueInstant,
+ final String recipient, final WebApplicationService service) {
+
+ final Response samlResponse = newSamlObject(Response.class);
+ samlResponse.setID(id);
+ samlResponse.setIssueInstant(issueInstant);
+ samlResponse.setVersion(SAMLVersion.VERSION_11);
+ samlResponse.setRecipient(recipient);
+ if (service instanceof SamlService) {
+ final SamlService samlService = (SamlService) service;
+
+ if (samlService.getRequestID() != null) {
+ samlResponse.setInResponseTo(samlService.getRequestID());
+ }
+ }
+ return samlResponse;
+ }
+
+ /**
+ * Create a new SAML1 response object.
+ *
+ * @param authnStatement the authn statement
+ * @param issuer the issuer
+ * @param issuedAt the issued at
+ * @param id the id
+ * @return the assertion
+ */
+ public Assertion newAssertion(final AuthenticationStatement authnStatement, final String issuer,
+ final DateTime issuedAt, final String id) {
+ final Assertion assertion = newSamlObject(Assertion.class);
+ assertion.setID(id);
+ assertion.setIssueInstant(issuedAt);
+ assertion.setIssuer(issuer);
+ assertion.getAuthenticationStatements().add(authnStatement);
+ return assertion;
+ }
+
+ /**
+ * New conditions element.
+ *
+ * @param issuedAt the issued at
+ * @param audienceUri the service id
+ * @param issueLength the issue length
+ * @return the conditions
+ */
+ public Conditions newConditions(final DateTime issuedAt, final String audienceUri, final long issueLength) {
+ final Conditions conditions = newSamlObject(Conditions.class);
+ conditions.setNotBefore(issuedAt);
+ conditions.setNotOnOrAfter(issuedAt.plus(issueLength));
+ final AudienceRestrictionCondition audienceRestriction = newSamlObject(AudienceRestrictionCondition.class);
+ final Audience audience = newSamlObject(Audience.class);
+ audience.setUri(audienceUri);
+ audienceRestriction.getAudiences().add(audience);
+ conditions.getAudienceRestrictionConditions().add(audienceRestriction);
+ return conditions;
+ }
+
+ /**
+ * Create a new SAML status object.
+ *
+ * @param codeValue the code value
+ * @param statusMessage the status message
+ * @return the status
+ */
+ public Status newStatus(final QName codeValue, final String statusMessage) {
+ final Status status = newSamlObject(Status.class);
+ final StatusCode code = newSamlObject(StatusCode.class);
+ code.setValue(codeValue);
+ status.setStatusCode(code);
+ if (statusMessage != null) {
+ final StatusMessage message = newSamlObject(StatusMessage.class);
+ message.setMessage(statusMessage);
+ status.setStatusMessage(message);
+ }
+ return status;
+ }
+
+ /**
+ * New authentication statement.
+ *
+ * @param authenticationDate the authentication date
+ * @param authenticationMethod the authentication method
+ * @param subjectId the subject id
+ * @return the authentication statement
+ */
+ public AuthenticationStatement newAuthenticationStatement(final Date authenticationDate,
+ final String authenticationMethod,
+ final String subjectId) {
+
+ final AuthenticationStatement authnStatement = newSamlObject(AuthenticationStatement.class);
+ authnStatement.setAuthenticationInstant(new DateTime(authenticationDate));
+ authnStatement.setAuthenticationMethod(
+ authenticationMethod != null
+ ? authenticationMethod
+ : SamlAuthenticationMetaDataPopulator.AUTHN_METHOD_UNSPECIFIED);
+ authnStatement.setSubject(newSubject(subjectId));
+ return authnStatement;
+ }
+
+ /**
+ * New subject element that uses the confirmation method
+ * {@link #CONFIRMATION_METHOD}.
+ *
+ * @param identifier the identifier
+ * @return the subject
+ */
+ public Subject newSubject(final String identifier) {
+ return newSubject(identifier, CONFIRMATION_METHOD);
+ }
+
+ /**
+ * New subject element with given confirmation method.
+ *
+ * @param identifier the identifier
+ * @param confirmationMethod the confirmation method
+ * @return the subject
+ */
+ public Subject newSubject(final String identifier, final String confirmationMethod) {
+ final SubjectConfirmation confirmation = newSamlObject(SubjectConfirmation.class);
+ final ConfirmationMethod method = newSamlObject(ConfirmationMethod.class);
+ method.setConfirmationMethod(confirmationMethod);
+ confirmation.getConfirmationMethods().add(method);
+ final NameIdentifier nameIdentifier = newSamlObject(NameIdentifier.class);
+ nameIdentifier.setNameIdentifier(identifier);
+ final Subject subject = newSamlObject(Subject.class);
+ subject.setNameIdentifier(nameIdentifier);
+ subject.setSubjectConfirmation(confirmation);
+ return subject;
+ }
+
+ /**
+ * New attribute statement.
+ *
+ * @param subject the subject
+ * @param attributes the attributes
+ * @param attributeNamespace the attribute namespace
+ * @return the attribute statement
+ */
+ public AttributeStatement newAttributeStatement(final Subject subject,
+ final Map attributes,
+ final String attributeNamespace) {
+
+ final AttributeStatement attrStatement = newSamlObject(AttributeStatement.class);
+ attrStatement.setSubject(subject);
+ for (final Map.Entry e : attributes.entrySet()) {
+ if (e.getValue() instanceof Collection> && ((Collection>) e.getValue()).isEmpty()) {
+ // bnoordhuis: don't add the attribute, it causes a org.opensaml.MalformedException
+ logger.info("Skipping attribute {} because it does not have any values.", e.getKey());
+ continue;
+ }
+ final Attribute attribute = newSamlObject(Attribute.class);
+ attribute.setAttributeName(e.getKey());
+ attribute.setAttributeNamespace(attributeNamespace);
+ if (e.getValue() instanceof Collection>) {
+ final Collection> c = (Collection>) e.getValue();
+ for (final Object value : c) {
+ attribute.getAttributeValues().add(newAttributeValue(value, AttributeValue.DEFAULT_ELEMENT_NAME));
+ }
+ } else {
+ attribute.getAttributeValues().add(newAttributeValue(e.getValue(), AttributeValue.DEFAULT_ELEMENT_NAME));
+ }
+ attrStatement.getAttributes().add(attribute);
+ }
+
+ return attrStatement;
+ }
+
+ /**
+ * Encode response and pass it onto the outbound transport.
+ * Uses {@link CasHTTPSOAP11Encoder} to handle encoding.
+ *
+ * @param httpResponse the http response
+ * @param httpRequest the http request
+ * @param samlMessage the saml response
+ * @throws Exception the exception in case encoding fails.
+ */
+ public void encodeSamlResponse(final HttpServletResponse httpResponse,
+ final HttpServletRequest httpRequest,
+ final Response samlMessage) throws Exception {
+ final BasicSAMLMessageContext messageContext = new BasicSAMLMessageContext();
+ messageContext.setOutboundMessageTransport(
+ new HttpServletResponseAdapter(httpResponse, httpRequest.isSecure()));
+ messageContext.setOutboundSAMLMessage(samlMessage);
+ this.encoder.encode(messageContext);
+ }
+}
diff --git a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/AbstractSaml10ResponseView.java b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/AbstractSaml10ResponseView.java
index f2012168b15a..f2c1d9eeaf4c 100644
--- a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/AbstractSaml10ResponseView.java
+++ b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/AbstractSaml10ResponseView.java
@@ -19,32 +19,17 @@
package org.jasig.cas.support.saml.web.view;
import org.jasig.cas.authentication.principal.WebApplicationService;
-import org.jasig.cas.support.saml.authentication.principal.SamlService;
-import org.jasig.cas.support.saml.util.CasHTTPSOAP11Encoder;
+import org.jasig.cas.support.saml.util.Saml10ObjectBuilder;
import org.jasig.cas.support.saml.web.support.SamlArgumentExtractor;
import org.jasig.cas.web.view.AbstractCasView;
import org.joda.time.DateTime;
-import org.opensaml.Configuration;
import org.opensaml.DefaultBootstrap;
-import org.opensaml.common.SAMLObject;
-import org.opensaml.common.SAMLObjectBuilder;
-import org.opensaml.common.SAMLVersion;
-import org.opensaml.common.binding.BasicSAMLMessageContext;
-import org.opensaml.common.impl.SecureRandomIdentifierGenerator;
-import org.opensaml.saml1.binding.encoding.HTTPSOAP11Encoder;
import org.opensaml.saml1.core.Response;
-import org.opensaml.saml1.core.Status;
-import org.opensaml.saml1.core.StatusCode;
-import org.opensaml.saml1.core.StatusMessage;
-import org.opensaml.ws.transport.http.HttpServletResponseAdapter;
import org.opensaml.xml.ConfigurationException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotNull;
-import javax.xml.namespace.QName;
-import java.lang.reflect.Field;
-import java.security.NoSuchAlgorithmException;
import java.util.Map;
/**
@@ -54,17 +39,14 @@
* @since 3.5.1
*/
public abstract class AbstractSaml10ResponseView extends AbstractCasView {
-
- private static final String DEFAULT_ELEMENT_NAME_FIELD = "DEFAULT_ELEMENT_NAME";
-
private static final String DEFAULT_ENCODING = "UTF-8";
- private final SamlArgumentExtractor samlArgumentExtractor = new SamlArgumentExtractor();
-
- private final HTTPSOAP11Encoder encoder = new CasHTTPSOAP11Encoder();
-
- private final SecureRandomIdentifierGenerator idGenerator;
+ /**
+ * The Saml object builder.
+ */
+ protected final Saml10ObjectBuilder samlObjectBuilder = new Saml10ObjectBuilder();
+ private final SamlArgumentExtractor samlArgumentExtractor = new SamlArgumentExtractor();
@NotNull
private String encoding = DEFAULT_ENCODING;
@@ -74,25 +56,12 @@ public abstract class AbstractSaml10ResponseView extends AbstractCasView {
static {
try {
- // Initialize OpenSAML default configuration
- // (only needed once per classloader)
DefaultBootstrap.bootstrap();
} catch (final ConfigurationException e) {
throw new IllegalStateException("Error initializing OpenSAML library.", e);
}
}
- /**
- * Instantiates a new abstract saml10 response view.
- */
- protected AbstractSaml10ResponseView() {
- try {
- this.idGenerator = new SecureRandomIdentifierGenerator();
- } catch (final NoSuchAlgorithmException e) {
- throw new IllegalStateException("Cannot create secure random ID generator for SAML message IDs.");
- }
- }
-
/**
* Sets the character encoding in the HTTP response.
*
@@ -102,8 +71,6 @@ public void setEncoding(final String encoding) {
this.encoding = encoding;
}
-
-
/**
* Sets the allowance for time skew in seconds
* between CAS and the client server. Default 0s.
@@ -139,24 +106,13 @@ protected void renderMergedOutputModel(
final String serviceId = service != null ? service.getId() : "UNKNOWN";
try {
- final Response samlResponse = newSamlObject(Response.class);
- samlResponse.setID(generateId());
- samlResponse.setIssueInstant(DateTime.now().minusSeconds(skewAllowance));
- samlResponse.setVersion(SAMLVersion.VERSION_11);
- samlResponse.setRecipient(serviceId);
- if (service instanceof SamlService) {
- final SamlService samlService = (SamlService) service;
-
- if (samlService.getRequestID() != null) {
- samlResponse.setInResponseTo(samlService.getRequestID());
- }
- }
+ final Response samlResponse = this.samlObjectBuilder.newResponse(
+ this.samlObjectBuilder.generateSecureRandomId(),
+ DateTime.now().minusSeconds(this.skewAllowance), serviceId, service);
+
prepareResponse(samlResponse, model);
- final BasicSAMLMessageContext messageContext = new BasicSAMLMessageContext();
- messageContext.setOutboundMessageTransport(new HttpServletResponseAdapter(response, request.isSecure()));
- messageContext.setOutboundSAMLMessage(samlResponse);
- this.encoder.encode(messageContext);
+ this.samlObjectBuilder.encodeSamlResponse(response, request, samlResponse);
} catch (final Exception e) {
logger.error("Error generating SAML response for service {}.", serviceId);
throw e;
@@ -172,57 +128,4 @@ protected void renderMergedOutputModel(
*/
protected abstract void prepareResponse(Response response, Map model);
-
- /**
- * Generate id.
- *
- * @return the string
- */
- protected final String generateId() {
- return this.idGenerator.generateIdentifier();
- }
-
- /**
- * Create a new SAML object.
- *
- * @param the generic type
- * @param objectType the object type
- * @return the t
- */
- protected final T newSamlObject(final Class objectType) {
- final QName qName;
- try {
- final Field f = objectType.getField(DEFAULT_ELEMENT_NAME_FIELD);
- qName = (QName) f.get(null);
- } catch (final NoSuchFieldException e) {
- throw new IllegalStateException("Cannot find field " + objectType.getName() + "." + DEFAULT_ELEMENT_NAME_FIELD);
- } catch (final IllegalAccessException e) {
- throw new IllegalStateException("Cannot access field " + objectType.getName() + "." + DEFAULT_ELEMENT_NAME_FIELD);
- }
- final SAMLObjectBuilder builder = (SAMLObjectBuilder) Configuration.getBuilderFactory().getBuilder(qName);
- if (builder == null) {
- throw new IllegalStateException("No SAMLObjectBuilder registered for class " + objectType.getName());
- }
- return objectType.cast(builder.buildObject());
- }
-
- /**
- * Create a new SAML status object.
- *
- * @param codeValue the code value
- * @param statusMessage the status message
- * @return the status
- */
- protected final Status newStatus(final QName codeValue, final String statusMessage) {
- final Status status = newSamlObject(Status.class);
- final StatusCode code = newSamlObject(StatusCode.class);
- code.setValue(codeValue);
- status.setStatusCode(code);
- if (statusMessage != null) {
- final StatusMessage message = newSamlObject(StatusMessage.class);
- message.setMessage(statusMessage);
- status.setStatusMessage(message);
- }
- return status;
- }
}
diff --git a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/Saml10FailureResponseView.java b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/Saml10FailureResponseView.java
index 054007bff8ca..800e1345a205 100644
--- a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/Saml10FailureResponseView.java
+++ b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/Saml10FailureResponseView.java
@@ -18,11 +18,11 @@
*/
package org.jasig.cas.support.saml.web.view;
-import java.util.Map;
-
import org.opensaml.saml1.core.Response;
import org.opensaml.saml1.core.StatusCode;
+import java.util.Map;
+
/**
* Represents a failed attempt at validating a ticket, responding via a SAML SOAP message.
*
@@ -34,6 +34,6 @@ public final class Saml10FailureResponseView extends AbstractSaml10ResponseView
@Override
protected void prepareResponse(final Response response, final Map model) {
- response.setStatus(newStatus(StatusCode.REQUEST_DENIED, (String) model.get("description")));
+ response.setStatus(this.samlObjectBuilder.newStatus(StatusCode.REQUEST_DENIED, (String) model.get("description")));
}
}
diff --git a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/Saml10SuccessResponseView.java b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/Saml10SuccessResponseView.java
index bd2ca6dbab37..915410c8ba32 100644
--- a/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/Saml10SuccessResponseView.java
+++ b/cas-server-support-saml/src/main/java/org/jasig/cas/support/saml/web/view/Saml10SuccessResponseView.java
@@ -25,28 +25,16 @@
import org.jasig.cas.support.saml.authentication.SamlAuthenticationMetaDataPopulator;
import org.joda.time.DateTime;
import org.opensaml.saml1.core.Assertion;
-import org.opensaml.saml1.core.Attribute;
-import org.opensaml.saml1.core.AttributeStatement;
-import org.opensaml.saml1.core.AttributeValue;
-import org.opensaml.saml1.core.Audience;
-import org.opensaml.saml1.core.AudienceRestrictionCondition;
import org.opensaml.saml1.core.AuthenticationStatement;
import org.opensaml.saml1.core.Conditions;
-import org.opensaml.saml1.core.ConfirmationMethod;
-import org.opensaml.saml1.core.NameIdentifier;
import org.opensaml.saml1.core.Response;
import org.opensaml.saml1.core.StatusCode;
import org.opensaml.saml1.core.Subject;
-import org.opensaml.saml1.core.SubjectConfirmation;
-import org.opensaml.xml.schema.XSString;
-import org.opensaml.xml.schema.impl.XSStringBuilder;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
-import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
-import java.util.Map.Entry;
/**
* Implementation of a view to return a SAML SOAP response and assertion, based on
@@ -67,12 +55,8 @@ public final class Saml10SuccessResponseView extends AbstractSaml10ResponseView
/** Namespace for custom attributes in the saml validation payload. */
private static final String VALIDATION_SAML_ATTRIBUTE_NAMESPACE = "http://www.ja-sig.org/products/cas/";
- private static final String CONFIRMATION_METHOD = "urn:oasis:names:tc:SAML:1.0:cm:artifact";
-
private static final int DEFAULT_ISSUE_LENGTH = 30000;
- private final XSStringBuilder attrValueBuilder = new XSStringBuilder();
-
/** The issuer, generally the hostname. */
@NotNull
private String issuer;
@@ -93,23 +77,27 @@ protected void prepareResponse(final Response response, final Map attributesToSend = prepareSamlAttributes(model);
if (!attributesToSend.isEmpty()) {
- assertion.getAttributeStatements().add(newAttributeStatement(subject, attributesToSend));
+ assertion.getAttributeStatements().add(this.samlObjectBuilder.newAttributeStatement(
+ subject, attributesToSend, VALIDATION_SAML_ATTRIBUTE_NAMESPACE));
}
- response.setStatus(newStatus(StatusCode.SUCCESS, null));
+ response.setStatus(this.samlObjectBuilder.newStatus(StatusCode.SUCCESS, null));
response.getAssertions().add(assertion);
}
@@ -124,7 +112,8 @@ protected void prepareResponse(final Response response, final Map prepareSamlAttributes(final Map model) {
- final Map authnAttributes = new HashMap(getAuthenticationAttributesAsMultiValuedAttributes(model));
+ final Map authnAttributes =
+ new HashMap(getAuthenticationAttributesAsMultiValuedAttributes(model));
if (isRememberMeAuthentication(model)) {
authnAttributes.remove(RememberMeCredential.AUTHENTICATION_ATTRIBUTE_REMEMBER_ME);
authnAttributes.put(this.rememberMeAttributeName, Boolean.TRUE.toString());
@@ -135,115 +124,6 @@ private Map prepareSamlAttributes(final Map mode
return attributesToReturn;
}
- /**
- * New conditions element.
- *
- * @param issuedAt the issued at
- * @param serviceId the service id
- * @return the conditions
- */
- private Conditions newConditions(final DateTime issuedAt, final String serviceId) {
- final Conditions conditions = newSamlObject(Conditions.class);
- conditions.setNotBefore(issuedAt);
- conditions.setNotOnOrAfter(issuedAt.plus(this.issueLength));
- final AudienceRestrictionCondition audienceRestriction = newSamlObject(AudienceRestrictionCondition.class);
- final Audience audience = newSamlObject(Audience.class);
- audience.setUri(serviceId);
- audienceRestriction.getAudiences().add(audience);
- conditions.getAudienceRestrictionConditions().add(audienceRestriction);
- return conditions;
- }
-
- /**
- * New subject element.
- *
- * @param identifier the identifier
- * @return the subject
- */
- private Subject newSubject(final String identifier) {
- final SubjectConfirmation confirmation = newSamlObject(SubjectConfirmation.class);
- final ConfirmationMethod method = newSamlObject(ConfirmationMethod.class);
- method.setConfirmationMethod(CONFIRMATION_METHOD);
- confirmation.getConfirmationMethods().add(method);
- final NameIdentifier nameIdentifier = newSamlObject(NameIdentifier.class);
- nameIdentifier.setNameIdentifier(identifier);
- final Subject subject = newSamlObject(Subject.class);
- subject.setNameIdentifier(nameIdentifier);
- subject.setSubjectConfirmation(confirmation);
- return subject;
- }
-
- /**
- * New authentication statement.
- *
- * @param model the model
- * @return the authentication statement
- */
- private AuthenticationStatement newAuthenticationStatement(final Map model) {
- final Authentication authentication = getPrimaryAuthenticationFrom(model);
- final String authenticationMethod = (String) authentication.getAttributes().get(
- SamlAuthenticationMetaDataPopulator.ATTRIBUTE_AUTHENTICATION_METHOD);
- final AuthenticationStatement authnStatement = newSamlObject(AuthenticationStatement.class);
- authnStatement.setAuthenticationInstant(new DateTime(authentication.getAuthenticationDate()));
- authnStatement.setAuthenticationMethod(
- authenticationMethod != null
- ? authenticationMethod
- : SamlAuthenticationMetaDataPopulator.AUTHN_METHOD_UNSPECIFIED);
- authnStatement.setSubject(newSubject(getPrincipal(model).getId()));
- return authnStatement;
- }
-
- /**
- * New attribute statement.
- *
- * @param subject the subject
- * @param attributes the attributes
- * @return the attribute statement
- */
- private AttributeStatement newAttributeStatement(
- final Subject subject, final Map attributes) {
-
- final AttributeStatement attrStatement = newSamlObject(AttributeStatement.class);
- attrStatement.setSubject(subject);
- for (final Entry e : attributes.entrySet()) {
- if (e.getValue() instanceof Collection> && ((Collection>) e.getValue()).isEmpty()) {
- // bnoordhuis: don't add the attribute, it causes a org.opensaml.MalformedException
- logger.info("Skipping attribute {} because it does not have any values.", e.getKey());
- continue;
- }
- final Attribute attribute = newSamlObject(Attribute.class);
- attribute.setAttributeName(e.getKey());
- attribute.setAttributeNamespace(VALIDATION_SAML_ATTRIBUTE_NAMESPACE);
- if (e.getValue() instanceof Collection>) {
- final Collection> c = (Collection>) e.getValue();
- for (final Object value : c) {
- attribute.getAttributeValues().add(newAttributeValue(value));
- }
- } else {
- attribute.getAttributeValues().add(newAttributeValue(e.getValue()));
- }
- attrStatement.getAttributes().add(attribute);
- }
-
- return attrStatement;
- }
-
- /**
- * New attribute value.
- *
- * @param value the value
- * @return the xS string
- */
- private XSString newAttributeValue(final Object value) {
- final XSString stringValue = this.attrValueBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME);
- if (value instanceof String) {
- stringValue.setValue((String) value);
- } else {
- stringValue.setValue(value.toString());
- }
- return stringValue;
- }
-
public void setIssueLength(final long issueLength) {
this.issueLength = issueLength;
}
diff --git a/cas-server-support-saml/src/test/java/org/jasig/cas/support/saml/authentication/principal/GoogleAccountsServiceTests.java b/cas-server-support-saml/src/test/java/org/jasig/cas/support/saml/authentication/principal/GoogleAccountsServiceTests.java
index 87048f623656..5ae85a35f4e0 100644
--- a/cas-server-support-saml/src/test/java/org/jasig/cas/support/saml/authentication/principal/GoogleAccountsServiceTests.java
+++ b/cas-server-support-saml/src/test/java/org/jasig/cas/support/saml/authentication/principal/GoogleAccountsServiceTests.java
@@ -18,32 +18,40 @@
*/
package org.jasig.cas.support.saml.authentication.principal;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.security.interfaces.DSAPrivateKey;
-import java.security.interfaces.DSAPublicKey;
-import java.util.zip.DeflaterOutputStream;
-
import org.apache.commons.codec.binary.Base64;
import org.jasig.cas.TestUtils;
+import org.jasig.cas.authentication.principal.Response;
import org.jasig.cas.authentication.principal.Service;
import org.jasig.cas.services.DefaultRegisteredServiceUsernameProvider;
import org.jasig.cas.services.RegisteredService;
import org.jasig.cas.services.ServicesManager;
+import org.jasig.cas.support.saml.SamlProtocolConstants;
import org.jasig.cas.util.PrivateKeyFactoryBean;
import org.jasig.cas.util.PublicKeyFactoryBean;
import org.junit.Before;
import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mock.web.MockHttpServletRequest;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.interfaces.DSAPrivateKey;
+import java.security.interfaces.DSAPublicKey;
+import java.util.zip.DeflaterOutputStream;
+
import static org.mockito.Mockito.*;
+import static org.junit.Assert.*;
+
/**
* @author Scott Battaglia
* @since 3.1
*/
public class GoogleAccountsServiceTests {
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
private GoogleAccountsService googleAccountsService;
public static GoogleAccountsService getGoogleAccountsService() throws Exception {
@@ -70,7 +78,8 @@ public static GoogleAccountsService getGoogleAccountsService() throws Exception
+ "ID=\"5545454455\" Version=\"2.0\" IssueInstant=\"Value\" "
+ "ProtocolBinding=\"urn:oasis:names.tc:SAML:2.0:bindings:HTTP-Redirect\" "
+ "ProviderName=\"https://localhost:8443/myRutgers\" AssertionConsumerServiceURL=\"https://localhost:8443/myRutgers\"/>";
- request.setParameter("SAMLRequest", encodeMessage(samlRequest));
+ request.setParameter(SamlProtocolConstants.PARAMETER_SAML_REQUEST, encodeMessage(samlRequest));
+ request.setParameter(SamlProtocolConstants.PARAMETER_SAML_RELAY_STATE, "RelayStateAddedHere");
final RegisteredService regSvc = mock(RegisteredService.class);
when(regSvc.getUsernameAttributeProvider()).thenReturn(new DefaultRegisteredServiceUsernameProvider());
@@ -87,22 +96,20 @@ public void setUp() throws Exception {
this.googleAccountsService.setPrincipal(TestUtils.getPrincipal());
}
-
- // XXX: re-enable when we figure out JVM requirements
@Test
public void verifyResponse() {
- return;
- // final Response response = this.googleAccountsService.getResponse("ticketId");
- // assertEquals(ResponseType.POST, response.getResponseType());
- // assertTrue(response.getAttributes().containsKey("SAMLResponse"));
+ final Response resp = this.googleAccountsService.getResponse("ticketId");
+ assertEquals(resp.getResponseType(), Response.ResponseType.POST);
+ assertTrue(resp.getAttributes().containsKey(SamlProtocolConstants.PARAMETER_SAML_RESPONSE));
+ assertTrue(resp.getAttributes().containsKey(SamlProtocolConstants.PARAMETER_SAML_RELAY_STATE));
+
}
- protected static String encodeMessage(final String xmlString) throws IOException {
+ private static String encodeMessage(final String xmlString) throws IOException {
final byte[] xmlBytes = xmlString.getBytes("UTF-8");
final ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
- final DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(
- byteOutputStream);
+ final DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteOutputStream);
deflaterOutputStream.write(xmlBytes, 0, xmlBytes.length);
deflaterOutputStream.close();